From 8ceaed067cb3fff201bed6c8358a6b95434640a2 Mon Sep 17 00:00:00 2001 From: Victor Shcherb Date: Wed, 31 Oct 2012 09:08:08 +0100 Subject: [PATCH] Introduce old router --- .../net/osmand/router/belarus_test.xml | 456 ------ .../net/osmand/router/germany_test.xml | 14 - .../src-tests/net/osmand/router/old_tests.zip | Bin 0 -> 43708 bytes .../osmand/router/BinaryRoutePlannerOld.java | 1229 +++++++++++++++++ 4 files changed, 1229 insertions(+), 470 deletions(-) delete mode 100644 DataExtractionOSM/src-tests/net/osmand/router/belarus_test.xml delete mode 100644 DataExtractionOSM/src-tests/net/osmand/router/germany_test.xml create mode 100644 DataExtractionOSM/src-tests/net/osmand/router/old_tests.zip create mode 100644 DataExtractionOSM/src/net/osmand/router/BinaryRoutePlannerOld.java diff --git a/DataExtractionOSM/src-tests/net/osmand/router/belarus_test.xml b/DataExtractionOSM/src-tests/net/osmand/router/belarus_test.xml deleted file mode 100644 index 48c6ea11dd..0000000000 --- a/DataExtractionOSM/src-tests/net/osmand/router/belarus_test.xml +++ /dev/null @@ -1,456 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DataExtractionOSM/src-tests/net/osmand/router/germany_test.xml b/DataExtractionOSM/src-tests/net/osmand/router/germany_test.xml deleted file mode 100644 index bc12649391..0000000000 --- a/DataExtractionOSM/src-tests/net/osmand/router/germany_test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/DataExtractionOSM/src-tests/net/osmand/router/old_tests.zip b/DataExtractionOSM/src-tests/net/osmand/router/old_tests.zip new file mode 100644 index 0000000000000000000000000000000000000000..d50dbcd3a71b2ad38ebdac4ef66ffa80dcae7711 GIT binary patch literal 43708 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 literal 0 HcmV?d00001 diff --git a/DataExtractionOSM/src/net/osmand/router/BinaryRoutePlannerOld.java b/DataExtractionOSM/src/net/osmand/router/BinaryRoutePlannerOld.java new file mode 100644 index 0000000000..a5ff6ae153 --- /dev/null +++ b/DataExtractionOSM/src/net/osmand/router/BinaryRoutePlannerOld.java @@ -0,0 +1,1229 @@ +package net.osmand.router; + +import gnu.trove.map.hash.TLongObjectHashMap; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.PriorityQueue; + +import net.osmand.LogUtil; +import net.osmand.binary.RouteDataObject; +import net.osmand.osm.MapRenderingTypes; +import net.osmand.osm.MapUtils; +import net.osmand.router.BinaryRoutePlanner.RouteSegment; + +import org.apache.commons.logging.Log; + +public class BinaryRoutePlannerOld { + + public static boolean PRINT_TO_CONSOLE_ROUTE_INFORMATION_TO_TEST = true; + private final int REVERSE_WAY_RESTRICTION_ONLY = 1024; + private final int STANDARD_ROAD_IN_QUEUE_OVERHEAD = 900; + + protected static final Log log = LogUtil.getLog(BinaryRoutePlannerOld.class); + + private static final int ROUTE_POINTS = 11; + private static final float TURN_DEGREE_MIN = 45; + + + private static double squareRootDist(int x1, int y1, int x2, int y2) { + // translate into meters + double dy = convert31YToMeters(y1, y2); + double dx = convert31XToMeters(x1, x2); + return Math.sqrt(dx * dx + dy * dy); +// return measuredDist(x1, y1, x2, y2); + } + + private static double measuredDist(int x1, int y1, int x2, int y2) { + return MapUtils.getDistance(MapUtils.get31LatitudeY(y1), MapUtils.get31LongitudeX(x1), + MapUtils.get31LatitudeY(y2), MapUtils.get31LongitudeX(x2)); + } + + private static double squareDist(int x1, int y1, int x2, int y2) { + // translate into meters + double dy = convert31YToMeters(y1, y2); + double dx = convert31XToMeters(x1, x2); + return dx * dx + dy * dy; + } + + private static double convert31YToMeters(int y1, int y2) { + // translate into meters + return (y1 - y2) * 0.01863d; + } + + private static double convert31XToMeters(int x1, int x2) { + // translate into meters + return (x1 - x2) * 0.011d; + } + + + private static double calculateProjection(int xA, int yA, int xB, int yB, int xC, int yC) { + // Scalar multiplication between (AB, AC) + double multiple = convert31XToMeters(xB, xA) * convert31XToMeters(xC, xA) + convert31YToMeters(yB, yA) * convert31YToMeters(yC, yA); + return multiple; + } + + + + public RouteSegment findRouteSegment(double lat, double lon, RoutingContext ctx) throws IOException { + int px = MapUtils.get31TileNumberX(lon); + int py = MapUtils.get31TileNumberY(lat); + ArrayList dataObjects = new ArrayList(); + ctx.loadTileData(px, py, 16, dataObjects); + if (dataObjects.isEmpty()) { + ctx.loadTileData(px, py, 14, dataObjects); + } + RouteSegment road = null; + double sdist = 0; + int foundProjX = 0; + int foundProjY = 0; + + for (RouteDataObject r : dataObjects) { + if (r.getPointsLength() > 1) { + for (int j = 1; j < r.getPointsLength(); j++) { + double mDist = squareRootDist(r.getPoint31XTile(j), r.getPoint31YTile(j), r.getPoint31XTile(j - 1), + r.getPoint31YTile(j - 1)); + int prx = r.getPoint31XTile(j); + int pry = r.getPoint31YTile(j); + double projection = calculateProjection(r.getPoint31XTile(j - 1), r.getPoint31YTile(j - 1), r.getPoint31XTile(j), + r.getPoint31YTile(j), px, py); + if (projection < 0) { + prx = r.getPoint31XTile(j - 1); + pry = r.getPoint31YTile(j - 1); + } else if (projection >= mDist * mDist) { + prx = r.getPoint31XTile(j); + pry = r.getPoint31YTile(j); + } else { + prx = (int) (r.getPoint31XTile(j - 1) + (r.getPoint31XTile(j) - r.getPoint31XTile(j - 1)) + * (projection / (mDist * mDist))); + pry = (int) (r.getPoint31YTile(j - 1) + (r.getPoint31YTile(j) - r.getPoint31YTile(j - 1)) + * (projection / (mDist * mDist))); + } + double currentsDist = squareDist(prx, pry, px, py); + if (road == null || currentsDist < sdist) { + RouteDataObject ro = new RouteDataObject(r); + road = new RouteSegment(ro, j); + ro.insert(j, prx, pry); + sdist = currentsDist; + foundProjX = prx; + foundProjY = pry; + } + } + } + } + if (road != null) { + // re-register the best road because one more point was inserted + ctx.registerRouteDataObject(foundProjX, foundProjY, road.getRoad()); + } + return road; + } + + public List searchRoute(final RoutingContext ctx, RouteSegment start, RouteSegment end, List intermediate, boolean leftSideNavigation) throws IOException, InterruptedException { + if(intermediate != null && intermediate.size() > 0) { + ArrayList ps = new ArrayList(intermediate); + ArrayList firstPartRecalculatedRoute = null; + ArrayList restPartRecalculatedRoute = null; + if (ctx.previouslyCalculatedRoute != null) { + List prev = ctx.previouslyCalculatedRoute; + long id = intermediate.get(0).getRoad().id; + int ss = intermediate.get(0).getSegmentStart(); + for (int i = 0; i < prev.size(); i++) { + RouteSegmentResult rsr = prev.get(i); + if (id == rsr.getObject().getId() && ss == rsr.getEndPointIndex()) { + firstPartRecalculatedRoute = new ArrayList(prev.subList(0, i + 1)); + restPartRecalculatedRoute = new ArrayList(prev.subList(i + 1, prev.size())); + break; + } + } + } + ps.add(end); + ps.add(0, start); + List results = new ArrayList(); + for (int i = 0; i < ps.size() - 1; i++) { + RoutingContext local = new RoutingContext(ctx); + if(i == 0) { + local.previouslyCalculatedRoute = firstPartRecalculatedRoute; + } + local.visitor = ctx.visitor; + List res = searchRouteInternalPrepare(local, ps.get(i), ps.get(i + 1), leftSideNavigation); + + results.addAll(res); + ctx.distinctLoadedTiles += local.distinctLoadedTiles; + ctx.loadedTiles += local.loadedTiles; + ctx.visitedSegments += local.visitedSegments; + ctx.loadedPrevUnloadedTiles += local.loadedPrevUnloadedTiles; + ctx.timeToCalculate += local.timeToCalculate; + ctx.timeToLoad += local.timeToLoad; + ctx.timeToLoadHeaders += local.timeToLoadHeaders; + ctx.relaxedSegments += local.relaxedSegments; + + local.unloadAllData(ctx); + if(restPartRecalculatedRoute != null) { + results.addAll(restPartRecalculatedRoute); + break; + } + } + ctx.unloadAllData(); + printResults(ctx, start, end, results); + return results; + } + return searchRoute(ctx, start, end, leftSideNavigation); + } + + @SuppressWarnings("static-access") + public List searchRoute(final RoutingContext ctx, RouteSegment start, RouteSegment end, boolean leftSideNavigation) throws IOException, InterruptedException { + if(ctx.SHOW_GC_SIZE){ + long h1 = ctx.runGCUsedMemory(); + float mb = (1 << 20); + log.warn("Used before routing " + h1 / mb+ " actual"); + } + List result = searchRouteInternalPrepare(ctx, start, end, leftSideNavigation); + if(result != null) { + printResults(ctx, start, end, result); + } + if (RoutingContext.SHOW_GC_SIZE) { + int sz = ctx.global.size; + log.warn("Subregion size " + ctx.subregionTiles.size() + " " + " tiles " + ctx.indexedSubregions.size()); + ctx.runGCUsedMemory(); + long h1 = ctx.runGCUsedMemory(); + ctx.unloadAllData(); + ctx.runGCUsedMemory(); + long h2 = ctx.runGCUsedMemory(); + float mb = (1 << 20); + log.warn("Unload context : estimated " + sz / mb + " ?= " + (h1 - h2) / mb + " actual"); + } + return result; + } + + + private List searchRouteInternalPrepare(final RoutingContext ctx, RouteSegment start, RouteSegment end, boolean leftSideNavigation) throws IOException, InterruptedException { + // Split into 2 methods to let GC work in between + searchRouteInternal(ctx, start, end, leftSideNavigation); + // 4. Route is found : collect all segments and prepare result + return prepareResult(ctx, start, end, leftSideNavigation); + } + + /** + * Calculate route between start.segmentEnd and end.segmentStart (using A* algorithm) + * return list of segments + */ + private void searchRouteInternal(final RoutingContext ctx, RouteSegment start, RouteSegment end, boolean leftSideNavigation) throws IOException, InterruptedException { + // measure time + ctx.timeToLoad = 0; + ctx.visitedSegments = 0; + ctx.timeToCalculate = System.nanoTime(); + if(ctx.config.initialDirection != null) { + ctx.firstRoadId = (start.getRoad().id << ROUTE_POINTS) + start.getSegmentStart(); + double plusDir = start.getRoad().directionRoute(start.segmentStart, true); + double diff = plusDir - ctx.config.initialDirection; + if(Math.abs(MapUtils.alignAngleDifference(diff)) <= Math.PI / 3) { + ctx.firstRoadDirection = 1; + } else if(Math.abs(MapUtils.alignAngleDifference(diff - Math.PI)) <= Math.PI / 3) { + ctx.firstRoadDirection = -1; + } + + } + + // Initializing priority queue to visit way segments + Comparator segmentsComparator = new Comparator(){ + @Override + public int compare(RouteSegment o1, RouteSegment o2) { + return ctx.roadPriorityComparator(o1.distanceFromStart, o1.distanceToEnd, o2.distanceFromStart, o2.distanceToEnd); + } + }; + + Comparator nonHeuristicSegmentsComparator = new Comparator(){ + @Override + public int compare(RouteSegment o1, RouteSegment o2) { + return roadPriorityComparator(o1.distanceFromStart, o1.distanceToEnd, o2.distanceFromStart, o2.distanceToEnd, 0.5); + } + }; + + PriorityQueue graphDirectSegments = new PriorityQueue(50, segmentsComparator); + PriorityQueue graphReverseSegments = new PriorityQueue(50, segmentsComparator); + + // Set to not visit one segment twice (stores road.id << X + segmentStart) + TLongObjectHashMap visitedDirectSegments = new TLongObjectHashMap(); + TLongObjectHashMap visitedOppositeSegments = new TLongObjectHashMap(); + + boolean runRecalculation = ctx.previouslyCalculatedRoute != null && ctx.previouslyCalculatedRoute.size() > 0; + if (runRecalculation) { + RouteSegment previous = null; + List rlist = new ArrayList(); + // always recalculate first 7 km + int distanceThreshold = 7000; + float threshold = 0; + for(RouteSegmentResult rr : ctx.previouslyCalculatedRoute) { + threshold += rr.getDistance(); + if(threshold > distanceThreshold) { + rlist.add(rr); + } + } + runRecalculation = rlist.size() > 0; + if (rlist.size() > 0) { + for (RouteSegmentResult rr : rlist) { + RouteSegment segment = new RouteSegment(rr.getObject(), rr.getEndPointIndex()); + if (previous != null) { + previous.parentRoute = segment; + previous.parentSegmentEnd = rr.getStartPointIndex(); + long t = (rr.getObject().getId() << ROUTE_POINTS) + segment.segmentStart; + visitedOppositeSegments.put(t, segment); + } + previous = segment; + } + end = previous; + } + } + + // for start : f(start) = g(start) + h(start) = 0 + h(start) = h(start) + int targetEndX = end.road.getPoint31XTile(end.segmentStart); + int targetEndY = end.road.getPoint31YTile(end.segmentStart); + int startX = start.road.getPoint31XTile(start.segmentStart); + int startY = start.road.getPoint31YTile(start.segmentStart); + float estimatedDistance = (float) h(ctx, targetEndX, targetEndY, startX, startY); + end.distanceToEnd = start.distanceToEnd = estimatedDistance; + + graphDirectSegments.add(start); + graphReverseSegments.add(end); + + // Extract & analyze segment with min(f(x)) from queue while final segment is not found + boolean inverse = false; + boolean init = false; + + PriorityQueue graphSegments; + if(inverse) { + graphSegments = graphReverseSegments; + } else { + graphSegments = graphDirectSegments; + } + while (!graphSegments.isEmpty()) { + RouteSegment segment = graphSegments.poll(); + ctx.visitedSegments++; + // for debug purposes + if (ctx.visitor != null) { + ctx.visitor.visitSegment(segment, true); + } + boolean routeFound = false; + if (!inverse) { + routeFound = processRouteSegment(ctx, false, graphDirectSegments, visitedDirectSegments, targetEndX, targetEndY, + segment, visitedOppositeSegments); + } else { + routeFound = processRouteSegment(ctx, true, graphReverseSegments, visitedOppositeSegments, startX, startY, segment, + visitedDirectSegments); + } + if (graphReverseSegments.isEmpty() || graphDirectSegments.isEmpty() || routeFound) { + break; + } + if(runRecalculation) { + // nothing to do + inverse = false; + } else if (!init) { + inverse = !inverse; + init = true; + } else if (ctx.planRouteIn2Directions()) { + inverse = nonHeuristicSegmentsComparator.compare(graphDirectSegments.peek(), graphReverseSegments.peek()) > 0; + if (graphDirectSegments.size() * 1.3 > graphReverseSegments.size()) { + inverse = true; + } else if (graphDirectSegments.size() < 1.3 * graphReverseSegments.size()) { + inverse = false; + } + } else { + // different strategy : use onedirectional graph + inverse = ctx.getPlanRoadDirection() < 0; + } + if (inverse) { + graphSegments = graphReverseSegments; + } else { + graphSegments = graphDirectSegments; + } + + if(ctx.runRelaxingStrategy() ) { + relaxNotNeededSegments(ctx, graphDirectSegments, true); + relaxNotNeededSegments(ctx, graphReverseSegments, false); + } + // check if interrupted + if(ctx.interruptable != null && ctx.interruptable.isCancelled()) { + throw new InterruptedException("Route calculation interrupted"); + } + } + println("Result is found"); + printDebugMemoryInformation(ctx, graphDirectSegments, graphReverseSegments, visitedDirectSegments, visitedOppositeSegments); + } + + + + private void relaxNotNeededSegments(RoutingContext ctx, PriorityQueue graphSegments, boolean inverse) { + RouteSegment next = graphSegments.peek(); + double mine = next.distanceToEnd; +// int before = graphSegments.size(); +// SegmentStat statStart = new SegmentStat("Distance from start (" + inverse + ") "); +// SegmentStat statEnd = new SegmentStat("Distance to end (" + inverse + ") "); + Iterator iterator = graphSegments.iterator(); + while (iterator.hasNext()) { + RouteSegment s = iterator.next(); +// statStart.addNumber((float) s.distanceFromStart); +// statEnd.addNumber((float) s.distanceToEnd); + if (s.distanceToEnd < mine) { + mine = s.distanceToEnd; + } + } + double d = mine * ctx.config.RELAX_NODES_IF_START_DIST_COEF; + iterator = graphSegments.iterator(); + while (iterator.hasNext()) { + RouteSegment s = iterator.next(); + if (s.distanceToEnd > d) { + ctx.relaxedSegments++; + iterator.remove(); + } + } +// int after = graphSegments.size(); +// println(statStart.toString()); +// println(statEnd.toString()); +// println("Relaxing : before " + before + " after " + after + " minend " + ((float) mine)); + } + + private double h(final RoutingContext ctx, int targetEndX, int targetEndY, + int startX, int startY) { + double distance = squareRootDist(startX, startY, targetEndX, targetEndY); + return distance / ctx.getRouter().getMaxDefaultSpeed(); + } + + protected static double h(RoutingContext ctx, double distToFinalPoint, RouteSegment next) { + double result = distToFinalPoint / ctx.getRouter().getMaxDefaultSpeed(); + if(ctx.isUseDynamicRoadPrioritising() && next != null){ + double priority = ctx.getRouter().getFutureRoadPriority(next.road); + result /= priority; + int dist = ctx.getDynamicRoadPriorityDistance(); + // only first N km-s count by dynamic priority + if(distToFinalPoint > dist && dist != 0){ + result = (distToFinalPoint - dist) / ctx.getRouter().getMaxDefaultSpeed() + + dist / (ctx.getRouter().getMaxDefaultSpeed() * priority); + } + } + return result; + } + + + + private static void println(String logMsg) { +// log.info(logMsg); + System.out.println(logMsg); + } + + private static void printInfo(String logMsg) { + log.warn(logMsg); + } + + public void printDebugMemoryInformation(RoutingContext ctx, PriorityQueue graphDirectSegments, PriorityQueue graphReverseSegments, + TLongObjectHashMap visitedDirectSegments,TLongObjectHashMap visitedOppositeSegments) { + printInfo("Time to calculate : " + (System.nanoTime() - ctx.timeToCalculate) / 1e6 + ", time to load : " + ctx.timeToLoad / 1e6 + ", time to load headers : " + ctx.timeToLoadHeaders / 1e6); + int maxLoadedTiles = Math.max(ctx.maxLoadedTiles, ctx.getCurrentlyLoadedTiles()); + printInfo("Current loaded tiles : " + ctx.getCurrentlyLoadedTiles() + ", maximum loaded tiles " + maxLoadedTiles); + printInfo("Loaded tiles " + ctx.loadedTiles + " (distinct "+ctx.distinctLoadedTiles+ "), unloaded tiles " + ctx.unloadedTiles + + ", loaded more than once same tiles " + + ctx.loadedPrevUnloadedTiles ); + printInfo("Visited roads, " + ctx.visitedSegments + ", relaxed roads " + ctx.relaxedSegments); + if (graphDirectSegments != null && graphReverseSegments != null) { + printInfo("Priority queues sizes : " + graphDirectSegments.size() + "/" + graphReverseSegments.size()); + } + if (visitedDirectSegments != null && visitedOppositeSegments != null) { + printInfo("Visited segments sizes: " + visitedDirectSegments.size() + "/" + visitedOppositeSegments.size()); + } + + } + + + private boolean processRouteSegment(final RoutingContext ctx, boolean reverseWaySearch, + PriorityQueue graphSegments, TLongObjectHashMap visitedSegments, int targetEndX, int targetEndY, + RouteSegment segment, TLongObjectHashMap oppositeSegments) throws IOException { + // Always start from segmentStart (!), not from segmentEnd + // It makes difference only for the first start segment + // Middle point will always be skipped from observation considering already visited + final RouteDataObject road = segment.road; + final int middle = segment.segmentStart; + double obstaclePlusTime = 0; + double obstacleMinusTime = 0; + + + // This is correct way of checking but it has problem with relaxing strategy +// long ntf = (segment.road.getId() << ROUTE_POINTS) + segment.segmentStart; +// visitedSegments.put(ntf, segment); +// if (oppositeSegments.contains(ntf) && oppositeSegments.get(ntf) != null) { +// RouteSegment opposite = oppositeSegments.get(ntf); +// if (opposite.segmentStart == segment.segmentStart) { + // if (reverseWaySearch) { + // reverse : segment.parentSegmentEnd - segment.parentRoute + // } else { + // reverse : opposite.parentSegmentEnd - oppositie.parentRoute + // } +// return true; +// } +// } + + // 0. mark route segment as visited + long nt = (road.getId() << ROUTE_POINTS) + middle; + // avoid empty segments to connect but mark the point as visited + visitedSegments.put(nt, null); + + int oneway = ctx.getRouter().isOneWay(road); + boolean minusAllowed; + boolean plusAllowed; + if(ctx.firstRoadId == nt) { + minusAllowed = ctx.firstRoadDirection <= 0; + plusAllowed = ctx.firstRoadDirection >= 0; + } else if (!reverseWaySearch) { + minusAllowed = oneway <= 0; + plusAllowed = oneway >= 0; + } else { + minusAllowed = oneway >= 0; + plusAllowed = oneway <= 0; + } + + // +/- diff from middle point + int d = plusAllowed ? 1 : -1; + if(segment.parentRoute != null) { + if(plusAllowed && middle < segment.getRoad().getPointsLength() - 1) { + obstaclePlusTime = ctx.getRouter().calculateTurnTime(segment, segment.getRoad().getPointsLength() - 1, + segment.parentRoute, segment.parentSegmentEnd); + } + if(minusAllowed && middle > 0) { + obstacleMinusTime = ctx.getRouter().calculateTurnTime(segment, 0, + segment.parentRoute, segment.parentSegmentEnd); + } + } + // Go through all point of the way and find ways to continue + // ! Actually there is small bug when there is restriction to move forward on way (it doesn't take into account) + double posSegmentDist = 0; + double negSegmentDist = 0; + while (minusAllowed || plusAllowed) { + // 1. calculate point not equal to middle + // (algorithm should visit all point on way if it is not oneway) + int segmentEnd = middle + d; + boolean positive = d > 0; + if (!minusAllowed && d > 0) { + d++; + } else if (!plusAllowed && d < 0) { + d--; + } else { + if (d <= 0) { + d = -d + 1; + } else { + d = -d; + } + } + if (segmentEnd < 0) { + minusAllowed = false; + continue; + } + if (segmentEnd >= road.getPointsLength()) { + plusAllowed = false; + continue; + } + // if we found end point break cycle + long nts = (road.getId() << ROUTE_POINTS) + segmentEnd; + visitedSegments.put(nts, segment); + + // 2. calculate point and try to load neighbor ways if they are not loaded + int x = road.getPoint31XTile(segmentEnd); + int y = road.getPoint31YTile(segmentEnd); + if(positive) { + posSegmentDist += squareRootDist(x, y, + road.getPoint31XTile(segmentEnd - 1), road.getPoint31YTile(segmentEnd - 1)); + } else { + negSegmentDist += squareRootDist(x, y, + road.getPoint31XTile(segmentEnd + 1), road.getPoint31YTile(segmentEnd + 1)); + } + + // 2.1 calculate possible obstacle plus time + if(positive){ + double obstacle = ctx.getRouter().defineRoutingObstacle(road, segmentEnd); + if (obstacle < 0) { + plusAllowed = false; + continue; + } + obstaclePlusTime += obstacle; + } else { + double obstacle = ctx.getRouter().defineRoutingObstacle(road, segmentEnd); + if (obstacle < 0) { + minusAllowed = false; + continue; + } + obstacleMinusTime += obstacle; + } +// int overhead = 0; + // could be expensive calculation + int overhead = (ctx.visitedSegments - ctx.relaxedSegments ) * + STANDARD_ROAD_IN_QUEUE_OVERHEAD; + if(overhead > ctx.config.memoryLimitation * 0.95){ + throw new OutOfMemoryError("There is no enough memory " + ctx.config.memoryLimitation/(1<<20) + " Mb"); + } + RouteSegment next = ctx.loadRouteSegment(x, y, ctx.config.memoryLimitation - overhead); + // 3. get intersected ways + if (next != null) { + // TO-DO U-Turn + if((next == segment || next.road.id == road.id) && next.next == null) { + // simplification if there is no real intersection + continue; + } + // Using A* routing algorithm + // g(x) - calculate distance to that point and calculate time + + double priority = ctx.getRouter().defineSpeedPriority(road); + double speed = ctx.getRouter().defineSpeed(road) * priority; + if (speed == 0) { + speed = ctx.getRouter().getMinDefaultSpeed() * priority; + } + double distOnRoadToPass = positive? posSegmentDist : negSegmentDist; + double distStartObstacles = segment.distanceFromStart + ( positive ? obstaclePlusTime : obstacleMinusTime) + + distOnRoadToPass / speed; + + double distToFinalPoint = squareRootDist(x, y, targetEndX, targetEndY); + boolean routeFound = processIntersections(ctx, graphSegments, visitedSegments, oppositeSegments, + distStartObstacles, distToFinalPoint, segment, segmentEnd, next, reverseWaySearch); + if(routeFound){ + return routeFound; + } + + } + } + return false; + } + + + private boolean proccessRestrictions(RoutingContext ctx, RouteDataObject road, RouteSegment inputNext, boolean reverseWay) { + ctx.segmentsToVisitPrescripted.clear(); + ctx.segmentsToVisitNotForbidden.clear(); + boolean exclusiveRestriction = false; + RouteSegment next = inputNext; + if (!reverseWay && road.getRestrictionLength() == 0) { + return false; + } + if(!ctx.getRouter().restrictionsAware()) { + return false; + } + while (next != null) { + int type = -1; + if (!reverseWay) { + for (int i = 0; i < road.getRestrictionLength(); i++) { + if (road.getRestrictionId(i) == next.road.id) { + type = road.getRestrictionType(i); + break; + } + } + } else { + for (int i = 0; i < next.road.getRestrictionLength(); i++) { + int rt = next.road.getRestrictionType(i); + long restrictedTo = next.road.getRestrictionId(i); + if (restrictedTo == road.id) { + type = rt; + break; + } + + // Check if there is restriction only to the other than current road + if (rt == MapRenderingTypes.RESTRICTION_ONLY_RIGHT_TURN || rt == MapRenderingTypes.RESTRICTION_ONLY_LEFT_TURN + || rt == MapRenderingTypes.RESTRICTION_ONLY_STRAIGHT_ON) { + // check if that restriction applies to considered junk + RouteSegment foundNext = inputNext; + while (foundNext != null) { + if (foundNext.getRoad().id == restrictedTo) { + break; + } + foundNext = foundNext.next; + } + if (foundNext != null) { + type = REVERSE_WAY_RESTRICTION_ONLY; // special constant + } + } + } + } + if (type == REVERSE_WAY_RESTRICTION_ONLY) { + // next = next.next; continue; + } else if (type == -1 && exclusiveRestriction) { + // next = next.next; continue; + } else if (type == MapRenderingTypes.RESTRICTION_NO_LEFT_TURN || type == MapRenderingTypes.RESTRICTION_NO_RIGHT_TURN + || type == MapRenderingTypes.RESTRICTION_NO_STRAIGHT_ON || type == MapRenderingTypes.RESTRICTION_NO_U_TURN) { + // next = next.next; continue; + } else if (type == -1) { + // case no restriction + ctx.segmentsToVisitNotForbidden.add(next); + } else { + // case exclusive restriction (only_right, only_straight, ...) + // 1. in case we are going backward we should not consider only_restriction + // as exclusive because we have many "in" roads and one "out" + // 2. in case we are going forward we have one "in" and many "out" + if (!reverseWay) { + exclusiveRestriction = true; + ctx.segmentsToVisitNotForbidden.clear(); + ctx.segmentsToVisitPrescripted.add(next); + } else { + ctx.segmentsToVisitNotForbidden.add(next); + } + } + next = next.next; + } + ctx.segmentsToVisitPrescripted.addAll(ctx.segmentsToVisitNotForbidden); + return true; + } + + + + + private boolean processIntersections(RoutingContext ctx, PriorityQueue graphSegments, + TLongObjectHashMap visitedSegments, TLongObjectHashMap oppositeSegments, + double distFromStart, double distToFinalPoint, + RouteSegment segment, int segmentEnd, RouteSegment inputNext, + boolean reverseWay) { + + boolean thereAreRestrictions = proccessRestrictions(ctx, segment.road, inputNext, reverseWay); + Iterator nextIterator = null; + if (thereAreRestrictions) { + nextIterator = ctx.segmentsToVisitPrescripted.iterator(); + } + // Calculate possible ways to put into priority queue + RouteSegment next = inputNext; + boolean hasNext = nextIterator == null || nextIterator.hasNext(); + while (hasNext) { + if (nextIterator != null) { + next = nextIterator.next(); + } + long nts = (next.road.getId() << ROUTE_POINTS) + next.segmentStart; + + // 1. Check if opposite segment found so we can stop calculations + if (oppositeSegments.contains(nts) && oppositeSegments.get(nts) != null) { + // restrictions checked + RouteSegment opposite = oppositeSegments.get(nts); + // additional check if opposite way not the same as current one + if (next.segmentStart != segmentEnd || + opposite.getRoad().getId() != segment.getRoad().getId()) { + if (reverseWay) { + ctx.finalReverseEndSegment = segmentEnd; + ctx.finalReverseRoute = segment; + ctx.finalDirectEndSegment = next.segmentStart; + ctx.finalDirectRoute = opposite; + } else { + ctx.finalDirectEndSegment = segmentEnd; + ctx.finalDirectRoute = segment; + ctx.finalReverseEndSegment = next.segmentStart; + ctx.finalReverseRoute = opposite; + } + return true; + } + } + // road.id could be equal on roundabout, but we should accept them + boolean alreadyVisited = visitedSegments.contains(nts); + if (!alreadyVisited) { + double distanceToEnd = h(ctx, distToFinalPoint, next); + if (next.parentRoute == null + || ctx.roadPriorityComparator(next.distanceFromStart, next.distanceToEnd, distFromStart, distanceToEnd) > 0) { + if (next.parentRoute != null) { + // already in queue remove it + if (!graphSegments.remove(next)) { + // exist in different queue! + RouteSegment cpy = new RouteSegment(next.getRoad(), next.segmentStart); + next = cpy; + } + } + next.distanceFromStart = distFromStart; + next.distanceToEnd = distanceToEnd; + // put additional information to recover whole route after + next.parentRoute = segment; + next.parentSegmentEnd = segmentEnd; + graphSegments.add(next); + } + if (ctx.visitor != null) { + ctx.visitor.visitSegment(next, false); + } + } else { + // the segment was already visited! We need to follow better route if it exists + // that is very strange situation and almost exception (it can happen when we underestimate distnceToEnd) + if (distFromStart < next.distanceFromStart && next.road.id != segment.road.id) { + // That code is incorrect (when segment is processed itself, + // then it tries to make wrong u-turn) - + // this situation should be very carefully checked in future (seems to be fixed) + // System.out.println(segment.getRoad().getName() + " " + next.getRoad().getName()); + // System.out.println(next.distanceFromStart + " ! " + distFromStart); + next.distanceFromStart = distFromStart; + next.parentRoute = segment; + next.parentSegmentEnd = segmentEnd; + if (ctx.visitor != null) { + ctx.visitor.visitSegment(next, false); + } + } + } + + // iterate to next road + if (nextIterator == null) { + next = next.next; + hasNext = next != null; + } else { + hasNext = nextIterator.hasNext(); + } + } + return false; + } + + + /** + * Helper method to prepare final result + */ + private List prepareResult(RoutingContext ctx, RouteSegment start, RouteSegment end, boolean leftside) { + List result = new ArrayList(); + + RouteSegment segment = ctx.finalReverseRoute; + int parentSegmentStart = ctx.finalReverseEndSegment; + while (segment != null) { + RouteSegmentResult res = new RouteSegmentResult(segment.road, parentSegmentStart, segment.segmentStart); + parentSegmentStart = segment.parentSegmentEnd; + segment = segment.parentRoute; + if(res.getStartPointIndex() != res.getEndPointIndex()) { + result.add(res); + } + } + Collections.reverse(result); + + segment = ctx.finalDirectRoute; + int parentSegmentEnd = ctx.finalDirectEndSegment; + while (segment != null) { + RouteSegmentResult res = new RouteSegmentResult(segment.road, segment.segmentStart, parentSegmentEnd); + parentSegmentEnd = segment.parentSegmentEnd; + segment = segment.parentRoute; + // happens in smart recalculation + if(res.getStartPointIndex() != res.getEndPointIndex()) { + result.add(res); + } + } + Collections.reverse(result); + + // calculate time + for (int i = 0; i < result.size(); i++) { + if(ctx.checkIfMemoryLimitCritical(ctx.config.memoryLimitation)) { + ctx.unloadUnusedTiles(ctx.config.memoryLimitation); + } + RouteSegmentResult rr = result.get(i); + RouteDataObject road = rr.getObject(); + double distOnRoadToPass = 0; + double speed = ctx.getRouter().defineSpeed(road); + if (speed == 0) { + speed = ctx.getRouter().getMinDefaultSpeed(); + } + boolean plus = rr.getStartPointIndex() < rr.getEndPointIndex(); + int next; + double distance = 0; + for (int j = rr.getStartPointIndex(); j != rr.getEndPointIndex(); j = next) { + next = plus ? j + 1 : j - 1; + if(j == rr.getStartPointIndex()) { + attachRoadSegments(ctx, result, i, j, plus); + } + if(next != rr.getEndPointIndex()) { + attachRoadSegments(ctx, result, i, next, plus); + } + + double d = measuredDist(road.getPoint31XTile(j), road.getPoint31YTile(j), road.getPoint31XTile(next), + road.getPoint31YTile(next)); + distance += d; + double obstacle = ctx.getRouter().defineObstacle(road, j); + if(obstacle < 0) { + obstacle = 0; + } + distOnRoadToPass += d / speed + obstacle; + + List attachedRoutes = rr.getAttachedRoutes(next); + if (next != rr.getEndPointIndex() && !rr.getObject().roundabout() && attachedRoutes != null) { + float before = rr.getBearing(next, !plus); + float after = rr.getBearing(next, plus); + boolean straight = Math.abs(MapUtils.degreesDiff(before + 180, after)) < TURN_DEGREE_MIN; + boolean split = false; + // split if needed + for (RouteSegmentResult rs : attachedRoutes) { + double diff = MapUtils.degreesDiff(before + 180, rs.getBearingBegin()); + if (Math.abs(diff) <= TURN_DEGREE_MIN) { + split = true; + } else if (!straight && Math.abs(diff) < 100) { + split = true; + } + } + if (split) { + int endPointIndex = rr.getEndPointIndex(); + RouteSegmentResult splitted = new RouteSegmentResult(rr.getObject(), next, endPointIndex); + rr.setSegmentTime((float) distOnRoadToPass); + rr.setSegmentSpeed((float) speed); + rr.setDistance((float) distance); + rr.setEndPointIndex(next); + + result.add(i + 1, splitted); + // switch current segment to the splitted + rr = splitted; + distOnRoadToPass = 0; + distance = 0; + } + } + } + // last point turn time can be added + // if(i + 1 < result.size()) { distOnRoadToPass += ctx.getRouter().calculateTurnTime(); } + rr.setSegmentTime((float) distOnRoadToPass); + rr.setSegmentSpeed((float) speed); + rr.setDistance((float) distance); + + + } + addTurnInfo(leftside, result); + return result; + } + + + private void printResults(RoutingContext ctx, RouteSegment start, RouteSegment end, List result) { + float completeTime = 0; + float completeDistance = 0; + for(RouteSegmentResult r : result) { + completeTime += r.getSegmentTime(); + completeDistance += r.getDistance(); + } + + println("ROUTE : "); + double startLat = MapUtils.get31LatitudeY(start.road.getPoint31YTile(start.segmentStart)); + double startLon = MapUtils.get31LongitudeX(start.road.getPoint31XTile(start.segmentStart)); + double endLat = MapUtils.get31LatitudeY(end.road.getPoint31YTile(end.segmentStart)); + double endLon = MapUtils.get31LongitudeX(end.road.getPoint31XTile(end.segmentStart)); + StringBuilder add = new StringBuilder(); + add.append("loadedTiles = \"").append(ctx.loadedTiles).append("\" "); + add.append("visitedSegments = \"").append(ctx.visitedSegments).append("\" "); + add.append("complete_distance = \"").append(completeDistance).append("\" "); + println(MessageFormat.format("", startLat + + "", startLon + "", endLat + "", endLon + "", completeTime + "", ctx.config.routerName, add.toString())); + if (PRINT_TO_CONSOLE_ROUTE_INFORMATION_TO_TEST) { + for (RouteSegmentResult res : result) { + String name = res.getObject().getName(); + String ref = res.getObject().getRef(); + if (name == null) { + name = ""; + } + if (ref != null) { + name += " (" + ref + ") "; + } + StringBuilder additional = new StringBuilder(); + additional.append("time = \"").append(res.getSegmentTime()).append("\" "); + additional.append("name = \"").append(name).append("\" "); + additional.append("distance = \"").append(res.getDistance()).append("\" "); + if (res.getTurnType() != null) { + additional.append("turn = \"").append(res.getTurnType()).append("\" "); + additional.append("turn_angle = \"").append(res.getTurnType().getTurnAngle()).append("\" "); + if (res.getTurnType().getLanes() != null) { + additional.append("lanes = \"").append(Arrays.toString(res.getTurnType().getLanes())).append("\" "); + } + } + additional.append("start_bearing = \"").append(res.getBearingBegin()).append("\" "); + additional.append("end_bearing = \"").append(res.getBearingEnd()).append("\" "); + additional.append("description = \"").append(res.getDescription()).append("\" "); + println(MessageFormat.format("\t", (res.getObject().getId()) + "", + res.getStartPointIndex() + "", res.getEndPointIndex() + "", additional.toString())); + } + } + println(""); + } + + + private void addTurnInfo(boolean leftside, List result) { + int prevSegment = -1; + float dist = 0; + int next = 1; + for (int i = 0; i <= result.size(); i = next) { + TurnType t = null; + next = i + 1; + if (i < result.size()) { + t = getTurnInfo(result, i, leftside); + // justify turn + if(t != null && i < result.size() - 1) { + boolean tl = TurnType.TL.equals(t.getValue()); + boolean tr = TurnType.TR.equals(t.getValue()); + if(tl || tr) { + TurnType tnext = getTurnInfo(result, i + 1, leftside); + if(tnext != null && result.get(i).getDistance() < 35) { + if(tl && TurnType.TL.equals(tnext.getValue()) ) { + next = i + 2; + t = TurnType.valueOf(TurnType.TU, false); + } else if(tr && TurnType.TR.equals(tnext.getValue()) ) { + next = i + 2; + t = TurnType.valueOf(TurnType.TU, true); + } + } + } + } + result.get(i).setTurnType(t); + } + if (t != null || i == result.size()) { + if (prevSegment >= 0) { + String turn = result.get(prevSegment).getTurnType().toString(); + if (result.get(prevSegment).getTurnType().getLanes() != null) { + turn += Arrays.toString(result.get(prevSegment).getTurnType().getLanes()); + } + result.get(prevSegment).setDescription(turn + String.format(" and go %.2f meters", dist)); + if(result.get(prevSegment).getTurnType().isSkipToSpeak()) { + result.get(prevSegment).setDescription(result.get(prevSegment).getDescription() +" (*)"); + } + } + prevSegment = i; + dist = 0; + } + if (i < result.size()) { + dist += result.get(i).getDistance(); + } + } + } + + private static final int MAX_SPEAK_PRIORITY = 5; + private int highwaySpeakPriority(String highway) { + if(highway == null || highway.endsWith("track") || highway.endsWith("services") || highway.endsWith("service") + || highway.endsWith("path")) { + return MAX_SPEAK_PRIORITY; + } + if (highway.endsWith("_link") || highway.endsWith("unclassified") || highway.endsWith("road") + || highway.endsWith("living_street") || highway.endsWith("residential") ) { + return 1; + } + return 0; + } + + + private TurnType getTurnInfo(List result, int i, boolean leftSide) { + if (i == 0) { + return TurnType.valueOf(TurnType.C, false); + } + RouteSegmentResult prev = result.get(i - 1) ; + if(prev.getObject().roundabout()) { + // already analyzed! + return null; + } + RouteSegmentResult rr = result.get(i); + if (rr.getObject().roundabout()) { + return processRoundaboutTurn(result, i, leftSide, prev, rr); + } + TurnType t = null; + if (prev != null) { + boolean noAttachedRoads = rr.getAttachedRoutes(rr.getStartPointIndex()).size() == 0; + // add description about turn + double mpi = MapUtils.degreesDiff(prev.getBearingEnd(), rr.getBearingBegin()); + if(noAttachedRoads){ + // TODO VICTOR : look at the comment inside direction route +// double begin = rr.getObject().directionRoute(rr.getStartPointIndex(), rr.getStartPointIndex() < +// rr.getEndPointIndex(), 25); +// mpi = MapUtils.degreesDiff(prev.getBearingEnd(), begin); + } + if (mpi >= TURN_DEGREE_MIN) { + if (mpi < 60) { + t = TurnType.valueOf(TurnType.TSLL, leftSide); + } else if (mpi < 120) { + t = TurnType.valueOf(TurnType.TL, leftSide); + } else if (mpi < 135 || leftSide) { + t = TurnType.valueOf(TurnType.TSHL, leftSide); + } else { + t = TurnType.valueOf(TurnType.TU, leftSide); + } + } else if (mpi < -TURN_DEGREE_MIN) { + if (mpi > -60) { + t = TurnType.valueOf(TurnType.TSLR, leftSide); + } else if (mpi > -120) { + t = TurnType.valueOf(TurnType.TR, leftSide); + } else if (mpi > -135 || !leftSide) { + t = TurnType.valueOf(TurnType.TSHR, leftSide); + } else { + t = TurnType.valueOf(TurnType.TU, leftSide); + } + } else { + t = attachKeepLeftInfoAndLanes(leftSide, prev, rr, t); + } + if (t != null) { + t.setTurnAngle((float) -mpi); + } + } + return t; + } + + + private TurnType processRoundaboutTurn(List result, int i, boolean leftSide, RouteSegmentResult prev, + RouteSegmentResult rr) { + int exit = 1; + RouteSegmentResult last = rr; + for (int j = i; j < result.size(); j++) { + RouteSegmentResult rnext = result.get(j); + last = rnext; + if (rnext.getObject().roundabout()) { + boolean plus = rnext.getStartPointIndex() < rnext.getEndPointIndex(); + int k = rnext.getStartPointIndex(); + if (j == i) { + k = plus ? k + 1 : k - 1; + } + while (k != rnext.getEndPointIndex()) { + if (rnext.getAttachedRoutes(k).size() > 0) { + exit++; + } + k = plus ? k + 1 : k - 1; + } + } else { + break; + } + } + // combine all roundabouts + TurnType t = TurnType.valueOf("EXIT"+exit, leftSide); + t.setTurnAngle((float) MapUtils.degreesDiff(last.getBearingBegin(), prev.getBearingEnd())) ; + return t; + } + + + private TurnType attachKeepLeftInfoAndLanes(boolean leftSide, RouteSegmentResult prev, RouteSegmentResult rr, TurnType t) { + // keep left/right + int[] lanes = null; + boolean kl = false; + boolean kr = false; + List attachedRoutes = rr.getAttachedRoutes(rr.getStartPointIndex()); + int ls = prev.getObject().getLanes(); + if(ls >= 0 && prev.getObject().getOneway() == 0) { + ls = (ls + 1) / 2; + } + int left = 0; + int right = 0; + boolean speak = false; + int speakPriority = Math.max(highwaySpeakPriority(prev.getObject().getHighway()), highwaySpeakPriority(rr.getObject().getHighway())); + if (attachedRoutes != null) { + for (RouteSegmentResult rs : attachedRoutes) { + double ex = MapUtils.degreesDiff(rs.getBearingBegin(), rr.getBearingBegin()); + double mpi = Math.abs(MapUtils.degreesDiff(prev.getBearingEnd(), rs.getBearingBegin())); + int rsSpeakPriority = highwaySpeakPriority(rs.getObject().getHighway()); + if (rsSpeakPriority != MAX_SPEAK_PRIORITY || speakPriority == MAX_SPEAK_PRIORITY) { + if ((ex < TURN_DEGREE_MIN || mpi < TURN_DEGREE_MIN) && ex >= 0) { + kl = true; + int lns = rs.getObject().getLanes(); + if(rs.getObject().getOneway() == 0) { + lns = (lns + 1) / 2; + } + if (lns > 0) { + right += lns; + } + speak = speak || rsSpeakPriority <= speakPriority; + } else if ((ex > -TURN_DEGREE_MIN || mpi < TURN_DEGREE_MIN) && ex <= 0) { + kr = true; + int lns = rs.getObject().getLanes(); + if(rs.getObject().getOneway() == 0) { + lns = (lns + 1) / 2; + } + if (lns > 0) { + left += lns; + } + speak = speak || rsSpeakPriority <= speakPriority; + } + } + } + } + if(kr && left == 0) { + left = 1; + } else if(kl && right == 0) { + right = 1; + } + int current = rr.getObject().getLanes(); + if(rr.getObject().getOneway() == 0) { + current = (current + 1) / 2; + } + if (current <= 0) { + current = 1; + } + if(ls >= 0 /*&& current + left + right >= ls*/){ + lanes = new int[current + left + right]; + ls = current + left + right; + for(int it=0; it< ls; it++) { + if(it < left || it >= left + current) { + lanes[it] = 0; + } else { + lanes[it] = 1; + } + } + // sometimes links are + if ((current <= left + right) && (left > 1 || right > 1)) { + speak = true; + } + } + + if (kl) { + t = TurnType.valueOf(TurnType.KL, leftSide); + t.setSkipToSpeak(!speak); + } else if(kr){ + t = TurnType.valueOf(TurnType.KR, leftSide); + t.setSkipToSpeak(!speak); + } + if (t != null && lanes != null) { + t.setLanes(lanes); + } + return t; + } + + private long getPoint(RouteDataObject road, int pointInd) { + return (((long) road.getPoint31XTile(pointInd)) << 31) + (long) road.getPoint31YTile(pointInd); + } + + + private void attachRoadSegments(RoutingContext ctx, List result, int routeInd, int pointInd, boolean plus) { + RouteSegmentResult rr = result.get(routeInd); + RouteDataObject road = rr.getObject(); + long nextL = pointInd < road.getPointsLength() - 1 ? getPoint(road, pointInd + 1) : 0; + long prevL = pointInd > 0 ? getPoint(road, pointInd - 1) : 0; + + // attach additional roads to represent more information about the route + RouteSegmentResult previousResult = null; + + // by default make same as this road id + long previousRoadId = road.getId(); + if (pointInd == rr.getStartPointIndex() && routeInd > 0) { + previousResult = result.get(routeInd - 1); + previousRoadId = previousResult.getObject().getId(); + if (previousRoadId != road.getId()) { + if (previousResult.getStartPointIndex() < previousResult.getEndPointIndex() + && previousResult.getEndPointIndex() < previousResult.getObject().getPointsLength() - 1) { + rr.attachRoute(pointInd, new RouteSegmentResult(previousResult.getObject(), previousResult.getEndPointIndex(), + previousResult.getObject().getPointsLength() - 1)); + } else if (previousResult.getStartPointIndex() > previousResult.getEndPointIndex() + && previousResult.getEndPointIndex() > 0) { + rr.attachRoute(pointInd, new RouteSegmentResult(previousResult.getObject(), previousResult.getEndPointIndex(), 0)); + } + } + } + RouteSegment routeSegment = ctx.loadRouteSegment(road.getPoint31XTile(pointInd), road.getPoint31YTile(pointInd), ctx.config.memoryLimitation); + // try to attach all segments except with current id + while (routeSegment != null) { + if (routeSegment.road.getId() != road.getId() && routeSegment.road.getId() != previousRoadId) { + RouteDataObject addRoad = routeSegment.road; + + // TODO restrictions can be considered as well + int oneWay = ctx.getRouter().isOneWay(addRoad); + if (oneWay >= 0 && routeSegment.segmentStart < addRoad.getPointsLength() - 1) { + long pointL = getPoint(addRoad, routeSegment.segmentStart + 1); + if(pointL != nextL && pointL != prevL) { + // if way contains same segment (nodes) as different way (do not attach it) + rr.attachRoute(pointInd, new RouteSegmentResult(addRoad, routeSegment.segmentStart, addRoad.getPointsLength() - 1)); + } + } + if (oneWay <= 0 && routeSegment.segmentStart > 0) { + long pointL = getPoint(addRoad, routeSegment.segmentStart - 1); + // if way contains same segment (nodes) as different way (do not attach it) + if(pointL != nextL && pointL != prevL) { + rr.attachRoute(pointInd, new RouteSegmentResult(addRoad, routeSegment.segmentStart, 0)); + } + } + } + routeSegment = routeSegment.next; + } + } + + + /*public */static int roadPriorityComparator(double o1DistanceFromStart, double o1DistanceToEnd, + double o2DistanceFromStart, double o2DistanceToEnd, double heuristicCoefficient ) { + // f(x) = g(x) + h(x) --- g(x) - distanceFromStart, h(x) - distanceToEnd (not exact) + return Double.compare(o1DistanceFromStart + heuristicCoefficient * o1DistanceToEnd, + o2DistanceFromStart + heuristicCoefficient * o2DistanceToEnd); + } + +}