From 6629569cb4e3692ac487757a83472d77badfae94 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 19 Jan 2016 17:22:00 +0100 Subject: [PATCH] new alternative route feature, merging #640 --- core/files/changelog.txt | 1 + .../java/com/graphhopper/AltResponse.java | 271 ++++++++ .../main/java/com/graphhopper/GHResponse.java | 235 ++----- .../java/com/graphhopper/GraphHopper.java | 163 +++-- .../routing/AbstractRoutingAlgorithm.java | 8 + .../graphhopper/routing/AlgorithmOptions.java | 8 + .../graphhopper/routing/AlternativeRoute.java | 619 ++++++++++++++++++ .../routing/DijkstraBidirectionRef.java | 4 +- .../java/com/graphhopper/routing/Path.java | 19 + .../routing/RoundTripAltAlgorithm.java | 283 ++++++++ .../graphhopper/routing/RoutingAlgorithm.java | 10 +- .../RoutingAlgorithmFactorySimple.java | 14 + .../util/BeelineWeightApproximator.java | 1 - .../routing/util/TestAlgoCollector.java | 10 +- .../java/com/graphhopper/util/GHUtility.java | 8 + .../java/com/graphhopper/util/PathMerger.java | 23 +- .../java/com/graphhopper/GHResponseTest.java | 19 +- .../com/graphhopper/GraphHopperAPITest.java | 18 +- .../java/com/graphhopper/GraphHopperIT.java | 124 ++-- .../java/com/graphhopper/GraphHopperTest.java | 85 +-- .../routing/AlternativeRouteTest.java | 185 ++++++ .../routing/RoundTripAltAlgorithmTest.java | 103 +++ .../com/graphhopper/tools/Measurement.java | 18 +- .../java/com/graphhopper/ui/MiniGraphUI.java | 57 +- .../graphhopper/http/GraphHopperServlet.java | 46 +- .../com/graphhopper/http/GraphHopperWeb.java | 60 +- .../http/SimpleRouteSerializer.java | 48 +- web/src/main/webapp/css/style.css | 80 ++- web/src/main/webapp/img/alt_route.png | Bin 0 -> 562 bytes web/src/main/webapp/js/instructions.js | 20 +- web/src/main/webapp/js/main-template.js | 143 ++-- web/src/main/webapp/js/main.js | 10 +- web/src/main/webapp/js/map.js | 37 +- .../http/GraphHopperServletIT.java | 20 +- .../graphhopper/http/GraphHopperWebTest.java | 14 +- 35 files changed, 2268 insertions(+), 496 deletions(-) create mode 100644 core/src/main/java/com/graphhopper/AltResponse.java create mode 100644 core/src/main/java/com/graphhopper/routing/AlternativeRoute.java create mode 100644 core/src/main/java/com/graphhopper/routing/RoundTripAltAlgorithm.java create mode 100644 core/src/test/java/com/graphhopper/routing/AlternativeRouteTest.java create mode 100644 core/src/test/java/com/graphhopper/routing/RoundTripAltAlgorithmTest.java create mode 100644 web/src/main/webapp/img/alt_route.png diff --git a/core/files/changelog.txt b/core/files/changelog.txt index d0d8675fcbb..d8e91c3b99e 100644 --- a/core/files/changelog.txt +++ b/core/files/changelog.txt @@ -1,4 +1,5 @@ 0.6 + GHResponse now wraps multiple AltResponse; renames GraphHopper.getPaths to calcPaths as 'get' sounds too cheap; a new method RoutingAlgorithm.calcPaths is added; see #596 moving lgpl licensed file into own submodule graphhopper-tools-lgpl renaming of Tarjans algorithm class to TarjansSCCAlgorithm more strict naming for Weighting enforced and more strict matching to select Weighting (equals check), #490 diff --git a/core/src/main/java/com/graphhopper/AltResponse.java b/core/src/main/java/com/graphhopper/AltResponse.java new file mode 100644 index 00000000000..74df2874b95 --- /dev/null +++ b/core/src/main/java/com/graphhopper/AltResponse.java @@ -0,0 +1,271 @@ +/* + * Licensed to GraphHopper and Peter Karich under one or more contributor + * license agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * GraphHopper 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; + +import com.graphhopper.util.InstructionList; +import com.graphhopper.util.PointList; +import com.graphhopper.util.shapes.BBox; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * This class holds one possibility of a route + *

+ * @author Peter Karich + */ +public class AltResponse +{ + private List description; + private double distance; + private double ascend; + private double descend; + private double routeWeight; + private long time; + private String debugInfo = ""; + private InstructionList instructions; + private PointList list = PointList.EMPTY; + private final List errors = new ArrayList(4); + + /** + * @return the description of this route alternative to make it meaningful for the user e.g. it + * displays one or two main roads of the route. + */ + public List getDescription() + { + if (description == null) + return Collections.emptyList(); + return description; + } + + public AltResponse setDescription( List names ) + { + this.description = names; + return this; + } + + public AltResponse addDebugInfo( String debugInfo ) + { + if (debugInfo == null) + throw new IllegalStateException("Debug information has to be none null"); + + if (!this.debugInfo.isEmpty()) + this.debugInfo += ";"; + + this.debugInfo += debugInfo; + return this; + } + + public String getDebugInfo() + { + return debugInfo; + } + + public AltResponse setPoints( PointList points ) + { + list = points; + return this; + } + + /** + * This method returns all points on the path. Keep in mind that calculating the distance from + * these points might yield different results compared to getDistance as points could have been + * simplified on import or after querying. + */ + public PointList getPoints() + { + check("getPoints"); + return list; + } + + public AltResponse setDistance( double distance ) + { + this.distance = distance; + return this; + } + + /** + * This method returns the distance of the path. Always prefer this method over + * getPoints().calcDistance + *

+ * @return distance in meter + */ + public double getDistance() + { + check("getDistance"); + return distance; + } + + public AltResponse setAscend( double ascend ) + { + if (ascend < 0) + throw new IllegalArgumentException("ascend has to be strictly positive"); + + this.ascend = ascend; + return this; + } + + /** + * This method returns the total elevation change (going upwards) in meter. + *

+ * @return ascend in meter + */ + public double getAscend() + { + return ascend; + } + + public AltResponse setDescend( double descend ) + { + if (descend < 0) + throw new IllegalArgumentException("descend has to be strictly positive"); + + this.descend = descend; + return this; + } + + /** + * This method returns the total elevation change (going downwards) in meter. + *

+ * @return decline in meter + */ + public double getDescend() + { + return descend; + } + + public AltResponse setTime( long timeInMillis ) + { + this.time = timeInMillis; + return this; + } + + /** + * @return time in millis + */ + public long getTime() + { + check("getTimes"); + return time; + } + + public AltResponse setRouteWeight( double weight ) + { + this.routeWeight = weight; + return this; + } + + /** + * This method returns a double value which is better than the time for comparison of routes but + * only if you know what you are doing, e.g. only to compare routes gained with the same query + * parameters like vehicle. + */ + public double getRouteWeight() + { + check("getRouteWeight"); + return routeWeight; + } + + /** + * Calculates the bounding box of this route response + */ + public BBox calcRouteBBox( BBox _fallback ) + { + check("calcRouteBBox"); + BBox bounds = BBox.createInverse(_fallback.hasElevation()); + int len = list.getSize(); + if (len == 0) + return _fallback; + + for (int i = 0; i < len; i++) + { + double lat = list.getLatitude(i); + double lon = list.getLongitude(i); + if (bounds.hasElevation()) + { + double ele = list.getEle(i); + bounds.update(lat, lon, ele); + } else + { + bounds.update(lat, lon); + } + } + return bounds; + } + + @Override + public String toString() + { + String str = "nodes:" + list.getSize() + "; " + list.toString(); + if (instructions != null && !instructions.isEmpty()) + str += ", " + instructions.toString(); + + if (hasErrors()) + str += ", " + errors.toString(); + + return str; + } + + public void setInstructions( InstructionList instructions ) + { + this.instructions = instructions; + } + + public InstructionList getInstructions() + { + check("getInstructions"); + if (instructions == null) + throw new IllegalArgumentException("To access instructions you need to enable creation before routing"); + + return instructions; + } + + private void check( String method ) + { + if (hasErrors()) + { + throw new RuntimeException("You cannot call " + method + " if response contains errors. Check this with ghResponse.hasErrors(). " + + "Errors are: " + getErrors()); + } + } + + /** + * @return true if this alternative response contains one or more errors + */ + public boolean hasErrors() + { + return !errors.isEmpty(); + } + + public List getErrors() + { + return errors; + } + + public AltResponse addError( Throwable error ) + { + errors.add(error); + return this; + } + + public AltResponse addErrors( List errors ) + { + this.errors.addAll(errors); + return this; + } +} diff --git a/core/src/main/java/com/graphhopper/GHResponse.java b/core/src/main/java/com/graphhopper/GHResponse.java index 0f4252ca41e..8df5bce36a3 100644 --- a/core/src/main/java/com/graphhopper/GHResponse.java +++ b/core/src/main/java/com/graphhopper/GHResponse.java @@ -17,10 +17,7 @@ */ package com.graphhopper; -import com.graphhopper.util.InstructionList; import com.graphhopper.util.PMap; -import com.graphhopper.util.PointList; -import com.graphhopper.util.shapes.BBox; import java.util.ArrayList; import java.util.List; @@ -34,230 +31,140 @@ public class GHResponse { private String debugInfo = ""; private final List errors = new ArrayList(4); - private PointList list = PointList.EMPTY; - private double distance; - private double ascend; - private double descend; - private double routeWeight; - private long time; - private InstructionList instructions; private final PMap hintsMap = new PMap(); + private final List alternatives = new ArrayList(5); public GHResponse() { } - public String getDebugInfo() - { - check("getDebugInfo"); - return debugInfo; - } - - public GHResponse setDebugInfo( String debugInfo ) - { - if (debugInfo != null) - this.debugInfo = debugInfo; - return this; - } - - private void check( String method ) + public void addAlternative( AltResponse altResponse ) { - if (hasErrors()) - { - throw new RuntimeException("You cannot call " + method + " if response contains errors. Check this with ghResponse.hasErrors(). " - + "Errors are: " + getErrors()); - } + alternatives.add(altResponse); } /** - * @return true if one or more error found + * Returns the first response. */ - public boolean hasErrors() + public AltResponse getFirst() { - return !errors.isEmpty(); - } + if (alternatives.isEmpty()) + throw new RuntimeException("Cannot fetch first alternative if list is empty"); - public List getErrors() - { - return errors; + return alternatives.get(0); } - @SuppressWarnings("unchecked") - public GHResponse addError( Throwable error ) + public List getAlternatives() { - errors.add(error); - return this; + return alternatives; } - public GHResponse setPoints( PointList points ) + public boolean hasAlternatives() { - list = points; - return this; + return !alternatives.isEmpty(); } - /** - * This method returns all points on the path. Keep in mind that calculating the distance from - * these point might yield different results compared to getDistance as points could have been - * simplified on import or after querying. - */ - public PointList getPoints() + public void addDebugInfo( String debugInfo ) { - check("getPoints"); - return list; - } + if (debugInfo == null) + throw new IllegalStateException("Debug information has to be none null"); - public GHResponse setDistance( double distance ) - { - this.distance = distance; - return this; - } + if (!this.debugInfo.isEmpty()) + this.debugInfo += ";"; - /** - * This method returns the distance of the path. Always prefer this method over - * getPoints().calcDistance - *

- * @return distance in meter - */ - public double getDistance() - { - check("getDistance"); - return distance; + this.debugInfo += debugInfo; } - public GHResponse setAscend( double ascend ) + public String getDebugInfo() { - if (ascend < 0) - throw new IllegalArgumentException("ascend has to be strictly positive"); + String str = debugInfo; + for (AltResponse ar : alternatives) + { + if (!str.isEmpty()) + str += "; "; - this.ascend = ascend; - return this; + str += ar.getDebugInfo(); + } + return str; } /** - * This method returns the total elevation change (going upwards) in meter. - *

- * @return ascend in meter + * This method returns true only if the response itself is errornous. + *

+ * @see #hasErrors() */ - public double getAscend() + public boolean hasRawErrors() { - return ascend; - } - - public GHResponse setDescend( double descend ) - { - if (descend < 0) - throw new IllegalArgumentException("descend has to be strictly positive"); - - this.descend = descend; - return this; + return !errors.isEmpty(); } /** - * This method returns the total elevation change (going downwards) in meter. - *

- * @return decline in meter + * This method returns true if no alternative is available, if one of these has an error or if + * the response itself is errornous. + *

+ * @see #hasRawErrors() */ - public double getDescend() + public boolean hasErrors() { - return descend; - } + if (hasRawErrors() || alternatives.isEmpty()) + return true; - public GHResponse setTime( long timeInMillis ) - { - this.time = timeInMillis; - return this; - } + for (AltResponse ar : alternatives) + { + if (ar.hasErrors()) + return true; + } - /** - * @return time in millis - * @deprecated use getTime instead - */ - public long getMillis() - { - check("getMillis"); - return time; + return false; } /** - * @return time in millis + * This method returns all the explicitely added errors and the errors of all alternatives. */ - public long getTime() + public List getErrors() { - check("getTimes"); - return time; + List list = new ArrayList(); + list.addAll(errors); + if (alternatives.isEmpty()) + list.add(new IllegalStateException("No alternative existent")); + else + for (AltResponse ar : alternatives) + { + list.addAll(ar.getErrors()); + } + return list; } - public GHResponse setRouteWeight( double weight ) + public GHResponse addErrors( List errors ) { - this.routeWeight = weight; + this.errors.addAll(errors); return this; } - /** - * This method returns a double value which is better than the time for comparison of routes but - * only if you know what you are doing, e.g. only to compare routes gained with the same query - * parameters like vehicle. - */ - public double getRouteWeight() + public GHResponse addError( Throwable error ) { - check("getRouteWeight"); - return routeWeight; + this.errors.add(error); + return this; } - /** - * Calculates the bounding box of this route response - */ - public BBox calcRouteBBox( BBox _fallback ) + @Override + public String toString() { - check("calcRouteBBox"); - BBox bounds = BBox.createInverse(_fallback.hasElevation()); - int len = list.getSize(); - if (len == 0) - return _fallback; - - for (int i = 0; i < len; i++) + String str = ""; + for (AltResponse a : alternatives) { - double lat = list.getLatitude(i); - double lon = list.getLongitude(i); - if (bounds.hasElevation()) - { - double ele = list.getEle(i); - bounds.update(lat, lon, ele); - } else - { - bounds.update(lat, lon); - } + str += "; " + a.toString(); } - return bounds; - } - @Override - public String toString() - { - String str = "nodes:" + list.getSize() + "; " + list.toString(); - if (instructions != null && !instructions.isEmpty()) - str += ", " + instructions.toString(); + if (alternatives.isEmpty()) + str = "no alternatives"; - if (hasErrors()) - str += ", " + errors.toString(); + if (!errors.isEmpty()) + str += ", main errors: " + errors.toString(); return str; } - public void setInstructions( InstructionList instructions ) - { - this.instructions = instructions; - } - - public InstructionList getInstructions() - { - check("getInstructions"); - if (instructions == null) - throw new IllegalArgumentException("To access instructions you need to enable creation before routing"); - - return instructions; - } - public PMap getHints() { return hintsMap; diff --git a/core/src/main/java/com/graphhopper/GraphHopper.java b/core/src/main/java/com/graphhopper/GraphHopper.java index e5ba0d3fa86..894aaff95db 100644 --- a/core/src/main/java/com/graphhopper/GraphHopper.java +++ b/core/src/main/java/com/graphhopper/GraphHopper.java @@ -1029,26 +1029,11 @@ public Weighting createTurnWeighting( Weighting weighting, Graph graph, FlagEnco public GHResponse route( GHRequest request ) { GHResponse response = new GHResponse(); - List paths = getPaths(request, response); - if (response.hasErrors()) - return response; - - boolean tmpEnableInstructions = request.getHints().getBool("instructions", enableInstructions); - boolean tmpCalcPoints = request.getHints().getBool("calcPoints", calcPoints); - double wayPointMaxDistance = request.getHints().getDouble("wayPointMaxDistance", 1d); - Locale locale = request.getLocale(); - DouglasPeucker peucker = new DouglasPeucker().setMaxDistance(wayPointMaxDistance); - - new PathMerger(). - setCalcPoints(tmpCalcPoints). - setDouglasPeucker(peucker). - setEnableInstructions(tmpEnableInstructions). - setSimplifyResponse(simplifyResponse && wayPointMaxDistance > 0). - doWork(response, paths, trMap.getWithFallBack(locale)); + calcPaths(request, response); return response; } - protected List getPaths( GHRequest request, GHResponse rsp ) + protected List calcPaths( GHRequest request, GHResponse ghRsp ) { if (ghStorage == null || !fullyLoaded) throw new IllegalStateException("Call load or importOrLoad before routing"); @@ -1062,7 +1047,7 @@ protected List getPaths( GHRequest request, GHResponse rsp ) if (!encodingManager.supports(vehicle)) { - rsp.addError(new IllegalArgumentException("Vehicle " + vehicle + " unsupported. " + ghRsp.addError(new IllegalArgumentException("Vehicle " + vehicle + " unsupported. " + "Supported are: " + getEncodingManager())); return Collections.emptyList(); } @@ -1074,41 +1059,21 @@ protected List getPaths( GHRequest request, GHResponse rsp ) tMode = TraversalMode.fromString(tModeStr); } catch (Exception ex) { - rsp.addError(ex); + ghRsp.addError(ex); return Collections.emptyList(); } - List points = request.getPoints(); - if (points.size() < 2) - { - rsp.addError(new IllegalStateException("At least 2 points have to be specified, but was:" + points.size())); - return Collections.emptyList(); - } - - long visitedNodesSum = 0; FlagEncoder encoder = encodingManager.getEncoder(vehicle); - EdgeFilter edgeFilter = new DefaultEdgeFilter(encoder); + List points = request.getPoints(); StopWatch sw = new StopWatch().start(); - List qResults = new ArrayList(points.size()); - for (int placeIndex = 0; placeIndex < points.size(); placeIndex++) - { - GHPoint point = points.get(placeIndex); - QueryResult res = locationIndex.findClosest(point.lat, point.lon, edgeFilter); - if (!res.isValid()) - rsp.addError(new IllegalArgumentException("Cannot find point " + placeIndex + ": " + point)); - - qResults.add(res); - } - - if (rsp.hasErrors()) + List qResults = lookup(points, encoder, ghRsp); + ghRsp.addDebugInfo("idLookup:" + sw.stop().getSeconds() + "s"); + if (ghRsp.hasRawErrors()) return Collections.emptyList(); - String debug = "idLookup:" + sw.stop().getSeconds() + "s"; - Weighting weighting; Graph routingGraph = ghStorage; - if (chEnabled) { boolean forceCHHeading = request.getHints().getBool("force_heading_ch", false); @@ -1124,7 +1089,7 @@ protected List getPaths( GHRequest request, GHResponse rsp ) queryGraph.lookup(qResults); weighting = createTurnWeighting(weighting, queryGraph, encoder); - List paths = new ArrayList(points.size() - 1); + List altPaths = new ArrayList(points.size() - 1); QueryResult fromQResult = qResults.get(0); double weightLimit = request.getHints().getDouble("defaultWeightLimit", defaultWeightLimit); @@ -1135,6 +1100,33 @@ protected List getPaths( GHRequest request, GHResponse rsp ) build(); boolean viaTurnPenalty = request.getHints().getBool("pass_through", false); + long visitedNodesSum = 0; + + boolean tmpEnableInstructions = request.getHints().getBool("instructions", enableInstructions); + boolean tmpCalcPoints = request.getHints().getBool("calcPoints", calcPoints); + double wayPointMaxDistance = request.getHints().getDouble("wayPointMaxDistance", 1d); + DouglasPeucker peucker = new DouglasPeucker().setMaxDistance(wayPointMaxDistance); + PathMerger pathMerger = new PathMerger(). + setCalcPoints(tmpCalcPoints). + setDouglasPeucker(peucker). + setEnableInstructions(tmpEnableInstructions). + setSimplifyResponse(simplifyResponse && wayPointMaxDistance > 0); + + Locale locale = request.getLocale(); + Translation tr = trMap.getWithFallBack(locale); + + // Every alternative path makes one AltResponse BUT if via points exists then reuse the altResponse object + AltResponse altResponse = new AltResponse(); + ghRsp.addAlternative(altResponse); + boolean isRoundTrip = AlgorithmOptions.ROUND_TRIP_ALT.equalsIgnoreCase(algoOpts.getAlgorithm()); + boolean isAlternativeRoute = AlgorithmOptions.ALT_ROUTE.equalsIgnoreCase(algoOpts.getAlgorithm()); + + if ((isAlternativeRoute || isRoundTrip) && points.size() > 2) + { + ghRsp.addError(new RuntimeException("Via points are not yet supported when alternative paths or round trips are requested. The returned paths would just need an additional identification for the via point index.")); + return Collections.emptyList(); + } + for (int placeIndex = 1; placeIndex < points.size(); placeIndex++) { if (placeIndex == 1) @@ -1143,8 +1135,12 @@ protected List getPaths( GHRequest request, GHResponse rsp ) queryGraph.enforceHeading(fromQResult.getClosestNode(), request.getFavoredHeading(0), false); } else if (viaTurnPenalty) { + if (isAlternativeRoute) + throw new IllegalStateException("Alternative paths and a viaTurnPenalty at the same time is currently not supported"); + // enforce straight start after via stop - EdgeIteratorState incomingVirtualEdge = paths.get(placeIndex - 2).getFinalEdge(); + Path prevRoute = altPaths.get(placeIndex - 2); + EdgeIteratorState incomingVirtualEdge = prevRoute.getFinalEdge(); queryGraph.enforceHeadingByEdgeId(fromQResult.getClosestNode(), incomingVirtualEdge.getEdge(), false); } @@ -1156,15 +1152,24 @@ protected List getPaths( GHRequest request, GHResponse rsp ) sw = new StopWatch().start(); RoutingAlgorithm algo = tmpAlgoFactory.createAlgo(queryGraph, algoOpts); algo.setWeightLimit(weightLimit); - debug += ", algoInit:" + sw.stop().getSeconds() + "s"; + String debug = ", algoInit:" + sw.stop().getSeconds() + "s"; sw = new StopWatch().start(); - Path path = algo.calcPath(fromQResult.getClosestNode(), toQResult.getClosestNode()); - if (path.getTime() < 0) - throw new RuntimeException("Time was negative. Please report as bug and include:" + request); + List pathList = algo.calcPaths(fromQResult.getClosestNode(), toQResult.getClosestNode()); + debug += ", " + algo.getName() + "-routing:" + sw.stop().getSeconds() + "s"; + if (pathList.isEmpty()) + throw new IllegalStateException("At least one path has to be returned for " + fromQResult + " -> " + toQResult); - paths.add(path); - debug += ", " + algo.getName() + "-routing:" + sw.stop().getSeconds() + "s, " + path.getDebugInfo(); + for (Path path : pathList) + { + if (path.getTime() < 0) + throw new RuntimeException("Time was negative. Please report as bug and include:" + request); + + altPaths.add(path); + debug += ", " + path.getDebugInfo(); + } + + altResponse.addDebugInfo(debug); // reset all direction enforcements in queryGraph to avoid influencing next path queryGraph.clearUnfavoredStatus(); @@ -1173,16 +1178,58 @@ protected List getPaths( GHRequest request, GHResponse rsp ) fromQResult = toQResult; } - if (rsp.hasErrors()) + if (isAlternativeRoute) + { + if (altPaths.isEmpty()) + throw new RuntimeException("Empty paths for alternative route calculation not expected"); + + // if alternative route calculation was done then create the responses from single paths + pathMerger.doWork(altResponse, Collections.singletonList(altPaths.get(0)), tr); + for (int index = 1; index < altPaths.size(); index++) + { + altResponse = new AltResponse(); + ghRsp.addAlternative(altResponse); + pathMerger.doWork(altResponse, Collections.singletonList(altPaths.get(index)), tr); + } + } else if (isRoundTrip) + { + if (points.size() != altPaths.size()) + throw new RuntimeException("There should be exactly one more points than paths. points:" + points.size() + ", paths:" + altPaths.size()); + + pathMerger.doWork(altResponse, altPaths, tr); + } else + { + if (points.size() - 1 != altPaths.size()) + throw new RuntimeException("There should be exactly one more points than paths. points:" + points.size() + ", paths:" + altPaths.size()); + + pathMerger.doWork(altResponse, altPaths, tr); + } + ghRsp.getHints().put("visited_nodes.sum", visitedNodesSum); + ghRsp.getHints().put("visited_nodes.average", (float) visitedNodesSum / (points.size() - 1)); + return altPaths; + } + + List lookup( List points, FlagEncoder encoder, GHResponse rsp ) + { + if (points.size() < 2) + { + rsp.addError(new IllegalStateException("At least 2 points have to be specified, but was:" + points.size())); return Collections.emptyList(); + } + + EdgeFilter edgeFilter = new DefaultEdgeFilter(encoder); + List qResults = new ArrayList(points.size()); + for (int placeIndex = 0; placeIndex < points.size(); placeIndex++) + { + GHPoint point = points.get(placeIndex); + QueryResult res = locationIndex.findClosest(point.lat, point.lon, edgeFilter); + if (!res.isValid()) + rsp.addError(new IllegalArgumentException("Cannot find point " + placeIndex + ": " + point)); - if (points.size() - 1 != paths.size()) - throw new RuntimeException("There should be exactly one more places than paths. places:" + points.size() + ", paths:" + paths.size()); + qResults.add(res); + } - rsp.setDebugInfo(debug); - rsp.getHints().put("visited_nodes.sum", visitedNodesSum); - rsp.getHints().put("visited_nodes.average", (float) visitedNodesSum / (points.size() - 1)); - return paths; + return qResults; } protected LocationIndex createLocationIndex( Directory dir ) diff --git a/core/src/main/java/com/graphhopper/routing/AbstractRoutingAlgorithm.java b/core/src/main/java/com/graphhopper/routing/AbstractRoutingAlgorithm.java index 874f724b7aa..1c6408574b0 100644 --- a/core/src/main/java/com/graphhopper/routing/AbstractRoutingAlgorithm.java +++ b/core/src/main/java/com/graphhopper/routing/AbstractRoutingAlgorithm.java @@ -24,6 +24,8 @@ import com.graphhopper.util.EdgeExplorer; import com.graphhopper.util.EdgeIterator; import com.graphhopper.util.EdgeIteratorState; +import java.util.Collections; +import java.util.List; /** * @author Peter Karich @@ -113,6 +115,12 @@ protected EdgeEntry createEdgeEntry( int node, double weight ) protected abstract boolean isWeightLimitExceeded(); + @Override + public List calcPaths( int from, int to ) + { + return Collections.singletonList(calcPath(from, to)); + } + protected Path createEmptyPath() { return new Path(graph, flagEncoder); diff --git a/core/src/main/java/com/graphhopper/routing/AlgorithmOptions.java b/core/src/main/java/com/graphhopper/routing/AlgorithmOptions.java index f34f4cf9581..6f0827f6a74 100644 --- a/core/src/main/java/com/graphhopper/routing/AlgorithmOptions.java +++ b/core/src/main/java/com/graphhopper/routing/AlgorithmOptions.java @@ -55,6 +55,14 @@ public class AlgorithmOptions * Bidirectional A* */ public static final String ASTAR_BI = "astarbi"; + /** + * alternative route algorithm + */ + public static final String ALT_ROUTE = "alternativeRoute"; + /** + * round trip algorithm based on alternative route algorithm + */ + public static final String ROUND_TRIP_ALT = "roundTripAlt"; private String algorithm = DIJKSTRA_BI; private Weighting weighting; private TraversalMode traversalMode = TraversalMode.NODE_BASED; diff --git a/core/src/main/java/com/graphhopper/routing/AlternativeRoute.java b/core/src/main/java/com/graphhopper/routing/AlternativeRoute.java new file mode 100644 index 00000000000..9bc3862d3b0 --- /dev/null +++ b/core/src/main/java/com/graphhopper/routing/AlternativeRoute.java @@ -0,0 +1,619 @@ +/* + * Licensed to GraphHopper and Peter Karich under one or more contributor + * license agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * GraphHopper 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 static com.graphhopper.routing.AlgorithmOptions.ALT_ROUTE; +import com.graphhopper.routing.util.EdgeFilter; +import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.routing.util.TraversalMode; +import com.graphhopper.routing.util.Weighting; +import com.graphhopper.storage.EdgeEntry; +import com.graphhopper.storage.Graph; +import com.graphhopper.util.EdgeIterator; +import com.graphhopper.util.EdgeIteratorState; +import com.graphhopper.util.GHUtility; +import gnu.trove.map.hash.TIntObjectHashMap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import gnu.trove.procedure.TIntObjectProcedure; +import gnu.trove.procedure.TObjectProcedure; +import gnu.trove.set.TIntSet; +import gnu.trove.set.hash.TIntHashSet; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class implements the alternative paths search using the "plateau" and partially the + * "penalty" method discribed in the following papers. + *

+ *

+ *

+ * + * @author Peter Karich + */ +public class AlternativeRoute implements RoutingAlgorithm +{ + private final Graph graph; + private final FlagEncoder flagEncoder; + private final Weighting weighting; + private final TraversalMode traversalMode; + private double weightLimit = Double.MAX_VALUE; + private int visitedNodes; + private double maxWeightFactor = 1.4; + // the higher the maxWeightFactor the higher the explorationFactor needs to be + private double maxExplorationFactor = 1; + + private double maxShareFactor = 0.6; + private double minPlateauFactor = 0.2; + private int maxPaths = 2; + + public AlternativeRoute( Graph graph, FlagEncoder flagEncoder, Weighting weighting, TraversalMode traversalMode ) + { + this.graph = graph; + this.flagEncoder = flagEncoder; + this.weighting = weighting; + this.traversalMode = traversalMode; + } + + @Override + public void setWeightLimit( double weightLimit ) + { + this.weightLimit = weightLimit; + } + + /** + * Increasing this factor results in returning more alternatives. E.g. if the factor is 2 than + * all alternatives with a weight 2 times longer than the optimal weight are return. (default is + * 1) + */ + public void setMaxWeightFactor( double maxWeightFactor ) + { + this.maxWeightFactor = maxWeightFactor; + } + + /** + * This parameter is used to avoid alternatives too similar to the best path. Specify 0.5 to + * force a same paths of maximum 50%. The unit is the 'weight' returned in the Weighting. + */ + public void setMaxShareFactor( double maxShareFactor ) + { + this.maxShareFactor = maxShareFactor; + } + + /** + * This method sets the minimum plateau portion of every alternative path that is required. + */ + public void setMinPlateauFactor( double minPlateauFactor ) + { + this.minPlateauFactor = minPlateauFactor; + } + + /** + * This method sets the graph exploration percentage for alternative paths. Default is 1 (100%). + * Specify a higher value to get more alternatives (especially if maxWeightFactor is higher than + * 1.5) and a lower value to improve query time but reduces the possibility to find + * alternatives. Similar to weightLimit but instead an absolute value a relative percentage to + * the best found path is used. + */ + public void setMaxExplorationFactor( double explorationFactor ) + { + this.maxExplorationFactor = explorationFactor; + } + + /** + * Specifies how many paths (including the optimal) are returned. (default is 2) + */ + public void setMaxPaths( int maxPaths ) + { + this.maxPaths = maxPaths; + if (this.maxPaths < 2) + throw new IllegalStateException("Use normal algorithm with less overhead instead if no alternatives are required"); + } + + /** + * This method calculates best paths (alternatives) between 'from' and 'to', where maxPaths-1 + * alternatives are searched and they are only accepted if they are not too similar but close to + * the best path. + */ + public List calcAlternatives( int from, int to ) + { + AltDijkstraBidirectionRef altBidirDijktra = new AltDijkstraBidirectionRef( + graph, flagEncoder, weighting, traversalMode, maxExplorationFactor * 2); + altBidirDijktra.searchBest(from, to); + altBidirDijktra.setWeightLimit(weightLimit); + visitedNodes = altBidirDijktra.getVisitedNodes(); + + List alternatives = altBidirDijktra. + calcAlternatives(maxPaths, maxWeightFactor, 10, maxShareFactor, 0.5, minPlateauFactor, -0.2); + return alternatives; + } + + @Override + public Path calcPath( int from, int to ) + { + return calcPaths(from, to).get(0); + } + + @Override + public List calcPaths( int from, int to ) + { + List alts = calcAlternatives(from, to); + List paths = new ArrayList(alts.size()); + for (AlternativeInfo a : alts) + { + paths.add(a.getPath()); + } + return paths; + } + + private static final Comparator ALT_COMPARATOR = new Comparator() + { + @Override + public int compare( AlternativeInfo o1, AlternativeInfo o2 ) + { + return Double.compare(o1.sortBy, o2.sortBy); + } + }; + + @Override + public String getName() + { + return ALT_ROUTE; + } + + @Override + public int getVisitedNodes() + { + return visitedNodes; + } + + public static class AlternativeInfo + { + private final double sortBy; + private final Path path; + private final EdgeEntry shareStart; + private final EdgeEntry shareEnd; + private final double shareWeight; + private final List names; + + public AlternativeInfo( double sortBy, Path path, EdgeEntry shareStart, EdgeEntry shareEnd, + double shareWeight, List altNames ) + { + this.names = altNames; + this.sortBy = sortBy; + this.path = path; + this.path.setDescription(names); + this.shareStart = shareStart; + this.shareEnd = shareEnd; + this.shareWeight = shareWeight; + } + + public Path getPath() + { + return path; + } + + public EdgeEntry getShareStart() + { + return shareStart; + } + + public EdgeEntry getShareEnd() + { + return shareEnd; + } + + public double getShareWeight() + { + return shareWeight; + } + + public double getSortBy() + { + return sortBy; + } + + @Override + public String toString() + { + return names + ", sortBy:" + sortBy + ", shareWeight:" + shareWeight + ", " + path; + } + } + + /** + * Helper class to find alternatives and alternatives for round trip. + */ + public static class AltDijkstraBidirectionRef extends DijkstraBidirectionRef + { + private final double explorationFactor; + + public AltDijkstraBidirectionRef( Graph graph, FlagEncoder encoder, Weighting weighting, TraversalMode tMode, + double explorationFactor ) + { + super(graph, encoder, weighting, tMode); + this.explorationFactor = explorationFactor; + } + + @Override + public boolean finished() + { + // we need to finish BOTH searches identical to CH + if (finishedFrom && finishedTo) + return true; + + if (currFrom.weight + currTo.weight > weightLimit) + return true; + + // The following condition is necessary to avoid traversing the full graph if areas are disconnected + // but it is only valid for none-CH e.g. for CH it can happen that finishedTo is true but the from-SPT could still reach 'to' + if (!bestPath.isFound() && (finishedFrom || finishedTo)) + return true; + + // increase overlap of both searches: + return currFrom.weight + currTo.weight > explorationFactor * bestPath.getWeight(); + // this is more precise but takes roughly 20% longer: + // return currFrom.weight > bestPath.getWeight() && currTo.weight > bestPath.getWeight(); + } + + public Path searchBest( int to, int from ) + { + createAndInitPath(); + initFrom(to, 0); + initTo(from, 0); + // init collections and bestPath.getWeight properly + runAlgo(); + return extractPath(); + } + + /** + * @return the information necessary to handle alternative paths. Note that the paths are + * not yet extracted. + */ + public List calcAlternatives( final int maxPaths, + double maxWeightFactor, final double weightInfluence, + final double maxShareFactor, final double shareInfluence, + final double minPlateauFactor, final double plateauInfluence ) + { + final double maxWeight = maxWeightFactor * bestPath.getWeight(); + final TIntObjectHashMap traversalIDMap = new TIntObjectHashMap(); + final AtomicInteger startTID = addToMap(traversalIDMap, bestPath); + + // find all 'good' alternatives from forward-SPT matching the backward-SPT and optimize by + // small total weight (1), small share and big plateau (3a+b) and do these expensive calculations + // only for plateau start candidates (2) + final List alternatives = new ArrayList(maxPaths); + + double bestPlateau = bestPath.getWeight(); + double bestShare = 0; + double sortBy = calcSortBy(weightInfluence, bestPath.getWeight(), + shareInfluence, bestShare, + plateauInfluence, bestPlateau); + + final AlternativeInfo bestAlt = new AlternativeInfo(sortBy, bestPath, + bestPath.edgeEntry, bestPath.edgeTo, bestShare, getAltNames(graph, bestPath.edgeEntry)); + alternatives.add(bestAlt); + final List bestPathEntries = new ArrayList(2); + + bestWeightMapFrom.forEachEntry(new TIntObjectProcedure() + { + @Override + public boolean execute( final int traversalId, final EdgeEntry fromEdgeEntry ) + { + EdgeEntry toEdgeEntry = bestWeightMapTo.get(traversalId); + if (toEdgeEntry == null) + return true; + + if (traversalMode.isEdgeBased()) + { + if (toEdgeEntry.parent != null) + // move to parent for two reasons: + // 1. make only turn costs missing in 'weight' and not duplicating current edge.weight + // 2. to avoid duplicate edge in Path + toEdgeEntry = toEdgeEntry.parent; + // TODO else if fromEdgeEntry.parent != null fromEdgeEntry = fromEdgeEntry.parent; + + } else + { + // The alternative path is suboptimal when both entries are parallel + if (fromEdgeEntry.edge == toEdgeEntry.edge) + return true; + } + + // (1) skip too long paths + final double weight = fromEdgeEntry.weight + toEdgeEntry.weight; + if (weight > maxWeight) + return true; + + // (2) Use the start traversal ID of a plateau as ID for the alternative path. + // Accept from-EdgeEntries only if such a start of a plateau + // i.e. discard if its parent has the same edgeId as the next to-EdgeEntry. + // Ignore already added best path + if (isBestPath(fromEdgeEntry, bestPath)) + return true; + + // For edge based traversal we need the next entry to find out the plateau start + EdgeEntry tmpFromEntry = traversalMode.isEdgeBased() ? fromEdgeEntry.parent : fromEdgeEntry; + if (tmpFromEntry == null || tmpFromEntry.parent == null) + { + // we can be here only if edge based and only if entry is not part of the best path + // e.g. when starting point has two edges and one is part of the best path the other edge is path of an alternative + assert traversalMode.isEdgeBased(); + } else + { + int nextToTraversalId = traversalMode.createTraversalId(tmpFromEntry.adjNode, + tmpFromEntry.parent.adjNode, tmpFromEntry.edge, true); + EdgeEntry tmpNextToEdgeEntry = bestWeightMapTo.get(nextToTraversalId); + if (tmpNextToEdgeEntry == null) + return true; + + if (traversalMode.isEdgeBased()) + tmpNextToEdgeEntry = tmpNextToEdgeEntry.parent; + // skip if on plateau + if (fromEdgeEntry.edge == tmpNextToEdgeEntry.edge) + return true; + } + + // (3a) calculate plateau, we know we are at the beginning of the 'from'-side of + // the plateau A-B-C and go further to B + // where B is the next-'from' of A and B is also the previous-'to' of A. + // + // *<-A-B-C->* + // / \ + // start end + // + // extend plateau in only one direction necessary (A to B to ...) as we know + // that the from-EdgeEntry is the start of the plateau or there is no plateau at all + // + double plateauWeight = 0; + EdgeEntry prevToEdgeEntry = toEdgeEntry; + // List plateauEdges = new ArrayList(); + while (prevToEdgeEntry.parent != null) + { + int nextFromTraversalId = traversalMode.createTraversalId(prevToEdgeEntry.adjNode, prevToEdgeEntry.parent.adjNode, + prevToEdgeEntry.edge, false); + + EdgeEntry nextFromEdgeEntry = bestWeightMapFrom.get(nextFromTraversalId); + // end of a plateau + if (nextFromEdgeEntry == null) + break; + + // is the next from-EdgeEntry on the plateau? + if (prevToEdgeEntry.edge != nextFromEdgeEntry.edge) + break; + + // plateauEdges.add(prevToEdgeEntry.edge); + plateauWeight += (prevToEdgeEntry.weight - prevToEdgeEntry.parent.weight); + prevToEdgeEntry = prevToEdgeEntry.parent; + } + + if (plateauWeight <= 0 || plateauWeight / weight < minPlateauFactor) + return true; + + if (fromEdgeEntry.parent == null) + throw new IllegalStateException("not implemented yet. in case of an edge based traversal the parent of fromEdgeEntry could be null"); + + // (3b) calculate share + EdgeEntry fromEE = getFirstShareEE(fromEdgeEntry.parent, true); + EdgeEntry toEE = getFirstShareEE(toEdgeEntry.parent, false); + double shareWeight = fromEE.weight + toEE.weight; + boolean smallShare = shareWeight / bestPath.getWeight() < maxShareFactor; + if (smallShare) + { + List altNames = getAltNames(graph, fromEdgeEntry); + double sortBy = calcSortBy(weightInfluence, weight, shareInfluence, shareWeight, plateauInfluence, plateauWeight); + double worstSortBy = getWorstSortBy(); + + // plateaus.add(new PlateauInfo(altName, plateauEdges)); + if (sortBy < worstSortBy || alternatives.size() < maxPaths) + { + Path path = new PathBidirRef(graph, flagEncoder). + setEdgeEntryTo(toEdgeEntry).setEdgeEntry(fromEdgeEntry). + setWeight(weight); + path.extract(); + + // for now do not add alternatives to set, if we do we need to remove then on alternatives.clear too (see below) + // AtomicInteger tid = addToMap(traversalIDMap, path); + // int tid = traversalMode.createTraversalId(path.calcEdges().get(0), false); + alternatives.add(new AlternativeInfo(sortBy, path, fromEE, toEE, shareWeight, altNames)); + + Collections.sort(alternatives, ALT_COMPARATOR); + if (alternatives.get(0) != bestAlt) + throw new IllegalStateException("best path should be always first entry"); + + if (alternatives.size() > maxPaths) + alternatives.subList(maxPaths, alternatives.size()).clear(); + } + } + + return true; + } + + /** + * Extract path until we stumble over an existing traversal id + */ + EdgeEntry getFirstShareEE( EdgeEntry startEE, boolean reverse ) + { + while (startEE.parent != null) + { + // TODO we could make use of traversal ID directly if stored in EdgeEntry + int tid = traversalMode.createTraversalId(startEE.adjNode, startEE.parent.adjNode, startEE.edge, reverse); + if (isAlreadyExisting(tid)) + return startEE; + + startEE = startEE.parent; + } + + return startEE; + } + + /** + * This method returns true if the specified tid is already existent in the + * traversalIDMap + */ + boolean isAlreadyExisting( final int tid ) + { + return !traversalIDMap.forEachValue(new TObjectProcedure() + { + @Override + public boolean execute( TIntSet set ) + { + return !set.contains(tid); + } + }); + } + + /** + * Return the current worst weight for all alternatives + */ + double getWorstSortBy() + { + if (alternatives.isEmpty()) + throw new IllegalStateException("Empty alternative list cannot happen"); + return alternatives.get(alternatives.size() - 1).sortBy; + } + + // returns true if fromEdgeEntry is identical to the specified best path + boolean isBestPath( EdgeEntry fromEdgeEntry, Path bestPath ) + { + if (traversalMode.isEdgeBased()) + { + if (GHUtility.getEdgeFromEdgeKey(startTID.get()) == fromEdgeEntry.edge) + { + if (fromEdgeEntry.parent == null) + throw new IllegalStateException("best path must have no parent but was non-null: " + fromEdgeEntry); + + return true; + } + + } else + { + if (fromEdgeEntry.parent == null) + { + bestPathEntries.add(fromEdgeEntry); + if (bestPathEntries.size() > 1) + throw new IllegalStateException("There is only one best path but was: " + bestPathEntries); + + if (startTID.get() != fromEdgeEntry.adjNode) + throw new IllegalStateException("Start traversal ID has to be identical to root edge entry " + + "which is the plateau start of the best path but was: " + startTID + " vs. adjNode: " + fromEdgeEntry.adjNode); + + return true; + } + } + + return false; + } + }); + + return alternatives; + } + + /** + * This method adds the traversal IDs of the specified path as set to the specified map. + */ + AtomicInteger addToMap( TIntObjectHashMap map, Path path ) + { + TIntSet set = new TIntHashSet(); + final AtomicInteger startTID = new AtomicInteger(-1); + for (EdgeIteratorState iterState : path.calcEdges()) + { + int tid = traversalMode.createTraversalId(iterState, false); + set.add(tid); + if (startTID.get() < 0) + { + // for node based traversal we need to explicitely add base node as starting node and to list + if (!traversalMode.isEdgeBased()) + { + tid = iterState.getBaseNode(); + set.add(tid); + } + + startTID.set(tid); + } + } + map.put(startTID.get(), set); + return startTID; + } + } + + static List getAltNames( Graph graph, EdgeEntry ee ) + { + if (ee == null) + return Collections.emptyList(); + + EdgeIteratorState iter = graph.getEdgeIteratorState(ee.edge, Integer.MIN_VALUE); + if (iter == null) + return Collections.emptyList(); + + String str = iter.getName(); + if (str.isEmpty()) + return Collections.emptyList(); + + return Collections.singletonList(str); + } + + static double calcSortBy( double weightInfluence, double weight, + double shareInfluence, double shareWeight, + double plateauInfluence, double plateauWeight ) + { + return weightInfluence * weight + shareInfluence * shareWeight + plateauInfluence * plateauWeight; + } + + public static class PlateauInfo + { + String name; + List edges; + + public PlateauInfo( String name, List edges ) + { + this.name = name; + this.edges = edges; + } + + @Override + public String toString() + { + return name; + } + + public List getEdges() + { + return edges; + } + + public String getName() + { + return name; + } + } +} diff --git a/core/src/main/java/com/graphhopper/routing/DijkstraBidirectionRef.java b/core/src/main/java/com/graphhopper/routing/DijkstraBidirectionRef.java index 47af01b4a3a..f78f64fe2dc 100644 --- a/core/src/main/java/com/graphhopper/routing/DijkstraBidirectionRef.java +++ b/core/src/main/java/com/graphhopper/routing/DijkstraBidirectionRef.java @@ -43,8 +43,8 @@ public class DijkstraBidirectionRef extends AbstractBidirAlgo { private PriorityQueue openSetFrom; private PriorityQueue openSetTo; - private TIntObjectMap bestWeightMapFrom; - private TIntObjectMap bestWeightMapTo; + protected TIntObjectMap bestWeightMapFrom; + protected TIntObjectMap bestWeightMapTo; protected TIntObjectMap bestWeightMapOther; protected EdgeEntry currFrom; protected EdgeEntry currTo; diff --git a/core/src/main/java/com/graphhopper/routing/Path.java b/core/src/main/java/com/graphhopper/routing/Path.java index ff3b5155b6b..419623e015c 100644 --- a/core/src/main/java/com/graphhopper/routing/Path.java +++ b/core/src/main/java/com/graphhopper/routing/Path.java @@ -27,6 +27,7 @@ import gnu.trove.list.array.TIntArrayList; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -41,6 +42,7 @@ public class Path { private static final AngleCalc ac = new AngleCalc(); + private List description; protected Graph graph; private FlagEncoder encoder; protected double distance; @@ -76,6 +78,23 @@ public Path( Graph graph, FlagEncoder encoder ) edgeEntry = p.edgeEntry; } + /** + * @return the description of this route alternative to make it meaningful for the user e.g. it + * displays one or two main roads of the route. + */ + public List getDescription() + { + if (description == null) + return Collections.emptyList(); + return description; + } + + public Path setDescription( List description ) + { + this.description = description; + return this; + } + public Path setEdgeEntry( EdgeEntry edgeEntry ) { this.edgeEntry = edgeEntry; diff --git a/core/src/main/java/com/graphhopper/routing/RoundTripAltAlgorithm.java b/core/src/main/java/com/graphhopper/routing/RoundTripAltAlgorithm.java new file mode 100644 index 00000000000..43a96db8b6c --- /dev/null +++ b/core/src/main/java/com/graphhopper/routing/RoundTripAltAlgorithm.java @@ -0,0 +1,283 @@ +/* + * Licensed to GraphHopper and Peter Karich under one or more contributor + * license agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * GraphHopper 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.FastestWeighting; +import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.routing.util.TraversalMode; +import com.graphhopper.routing.util.Weighting; +import com.graphhopper.storage.EdgeEntry; +import com.graphhopper.storage.Graph; +import com.graphhopper.storage.NodeAccess; +import com.graphhopper.util.DistanceCalc; +import com.graphhopper.util.EdgeIterator; +import com.graphhopper.util.EdgeIteratorState; +import com.graphhopper.util.Helper; +import gnu.trove.set.hash.TIntHashSet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * This class implements the round trip calculation via calculating the optimal path between the + * origin and the best point on the "outside Dijkstra border" and uses an alternative path algorithm + * as backward path. + *

+ * @author Peter Karich + */ +public class RoundTripAltAlgorithm implements RoutingAlgorithm +{ + private final Graph graph; + private final FlagEncoder flagEncoder; + private final Weighting weighting; + private final TraversalMode traversalMode; + private double weightLimit = Double.MAX_VALUE; + private int visitedNodes; + private double maxWeightFactor = 2; + + public RoundTripAltAlgorithm( Graph graph, FlagEncoder flagEncoder, Weighting weighting, TraversalMode traversalMode ) + { + this.graph = graph; + this.flagEncoder = flagEncoder; + this.weighting = weighting; + + this.traversalMode = traversalMode; + if (this.traversalMode != TraversalMode.NODE_BASED) + throw new IllegalArgumentException("Only node based traversal currently supported for round trip calculation"); + } + + public void setMaxWeightFactor( double maxWeightFactor ) + { + this.maxWeightFactor = maxWeightFactor; + } + + /** + * @param from the node where the round trip should start and end + * @param maxFullDistance the maximum distance for the whole round trip + * @return currently no path at all or two paths (one forward and one backward path) + */ + public List calcRoundTrips( int from, double maxFullDistance, final double penaltyFactor ) + { + AltSingleDijkstra altDijkstra = new AltSingleDijkstra(graph, flagEncoder, weighting, traversalMode); + altDijkstra.setWeightLimit(weightLimit); + altDijkstra.beforeRun(from); + EdgeEntry currFrom = altDijkstra.searchBest(from, maxFullDistance); + visitedNodes = altDijkstra.getVisitedNodes(); + if (currFrom == null) + return Collections.emptyList(); + + // Assume that the first node breaking through the maxWeight circle is the best connected leading hopefully to good alternatives + // TODO select more than one 'to'-node? + int to = currFrom.adjNode; + + // TODO do not extract yet, use the plateau start of the alternative as new 'to', then extract + final TIntHashSet forwardEdgeSet = new TIntHashSet(); + + // best path FOR FORWARD direction which we need in all cases + Path bestForwardPath = new Path(graph, flagEncoder) + { + @Override + protected void processEdge( int edgeId, int adjNode ) + { + super.processEdge(edgeId, adjNode); + forwardEdgeSet.add(edgeId); + } + }; + + bestForwardPath.setEdgeEntry(currFrom); + bestForwardPath.setWeight(currFrom.weight); + bestForwardPath.extract(); + if (forwardEdgeSet.isEmpty()) + return Collections.emptyList(); + + List paths = new ArrayList(); + // simple penalty method + Weighting altWeighting = new FastestWeighting(flagEncoder) + { + @Override + public double calcWeight( EdgeIteratorState edge, boolean reverse, int prevOrNextEdgeId ) + { + double factor = 1; + if (forwardEdgeSet.contains(edge.getEdge())) + factor = penaltyFactor; + return factor * weighting.calcWeight(edge, reverse, prevOrNextEdgeId); + } + }; + AlternativeRoute.AltDijkstraBidirectionRef altBidirDijktra = new AlternativeRoute.AltDijkstraBidirectionRef(graph, flagEncoder, + altWeighting, traversalMode, 1); + altBidirDijktra.setWeightLimit(weightLimit); + // find an alternative for backward direction starting from 'to' + Path bestBackwardPath = altBidirDijktra.searchBest(to, from); + + // path not found -> TODO try with another 'to' point + if (Double.isInfinite(bestBackwardPath.getWeight())) + return Collections.emptyList(); + + // less weight influence, stronger share avoiding than normal alternative search to increase area between best&alternative + double weightInfluence = 0.05, maxShareFactor = 0.05, shareInfluence = 2 /*use penaltyFactor?*/, + minPlateauFactor = 0.1, plateauInfluence = 0.1; + List infos = altBidirDijktra.calcAlternatives(2, + penaltyFactor * maxWeightFactor, weightInfluence, + maxShareFactor, shareInfluence, + minPlateauFactor, plateauInfluence); + + visitedNodes += altBidirDijktra.getVisitedNodes(); + if (infos.isEmpty()) + return Collections.emptyList(); + + if (infos.size() == 1) + { + // fallback to same path for backward direction (or at least VERY similar path as optimal) + paths.add(bestForwardPath); + paths.add(infos.get(0).getPath()); + } else + { + AlternativeRoute.AlternativeInfo secondBest = null; + for (AlternativeRoute.AlternativeInfo i : infos) + { + if (1 - i.getShareWeight() / i.getPath().getWeight() > 1e-8) + { + secondBest = i; + break; + } + } + if (secondBest == null) + throw new RuntimeException("no second best found. " + infos); + + // correction: remove end standing path + EdgeEntry newTo = secondBest.getShareStart(); + if (newTo.parent != null) + { + // in case edge was found in forwardEdgeSet we calculate the first sharing end + int tKey = traversalMode.createTraversalId(newTo.adjNode, newTo.parent.adjNode, newTo.edge, false); + + // do new extract + EdgeEntry tmpFromEdgeEntry = altDijkstra.getFromEntry(tKey); + + // if (tmpFromEdgeEntry.parent != null) tmpFromEdgeEntry = tmpFromEdgeEntry.parent; + bestForwardPath = new Path(graph, flagEncoder).setEdgeEntry(tmpFromEdgeEntry).setWeight(tmpFromEdgeEntry.weight).extract(); + + newTo = newTo.parent; + // force new 'to' + newTo.edge = EdgeIterator.NO_EDGE; + secondBest.getPath().setWeight(secondBest.getPath().getWeight() - newTo.weight).extract(); + } + + paths.add(bestForwardPath); + paths.add(secondBest.getPath()); + } + return paths; + } + + @Override + public Path calcPath( int from, int to ) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public List calcPaths( int from, int to ) + { + // TODO use to-point to indicate direction too, not only distance + double fromLat = graph.getNodeAccess().getLat(from), fromLon = graph.getNodeAccess().getLon(from); + double toLat = graph.getNodeAccess().getLat(to), toLon = graph.getNodeAccess().getLon(to); + + double maxDist = Helper.DIST_EARTH.calcDist(fromLat, fromLon, toLat, toLon) * 2; + double penaltyFactor = 2; + return calcRoundTrips(from, maxDist, penaltyFactor); + } + + @Override + public void setWeightLimit( double weightLimit ) + { + this.weightLimit = weightLimit; + } + + @Override + public String getName() + { + return AlgorithmOptions.ROUND_TRIP_ALT; + } + + @Override + public int getVisitedNodes() + { + return visitedNodes; + } + + /** + * Helper class for one to many dijkstra + */ + static class AltSingleDijkstra extends DijkstraBidirectionRef + { + public AltSingleDijkstra( Graph g, FlagEncoder encoder, Weighting weighting, TraversalMode tMode ) + { + super(g, encoder, weighting, tMode); + } + + void beforeRun( int from ) + { + checkAlreadyRun(); + createAndInitPath(); + initFrom(from, 0); + } + + public EdgeEntry getFromEntry( int key ) + { + return bestWeightMapFrom.get(key); + } + + EdgeEntry searchBest( int from, double maxFullDistance ) + { + NodeAccess na = graph.getNodeAccess(); + DistanceCalc distanceCalc = Helper.DIST_PLANE; + // get the 'to' via exploring the graph and select the node which reaches the maxWeight radius the fastest! + // '/2' because we need just one direction + double maxDistance = distanceCalc.calcNormalizedDist(maxFullDistance / 2); + double lat1 = na.getLatitude(from), lon1 = na.getLongitude(from); + double lastNormedDistance = -1; + boolean tmpFinishedFrom = false; + EdgeEntry tmp = null; + + while (!tmpFinishedFrom) + { + tmpFinishedFrom = !fillEdgesFrom(); + + // DO NOT use currFrom.adjNode and instead use parent as currFrom can contain + // a very big weight making it an unreasonable goal + // (think about "avoid motorway" and see #419) + tmp = currFrom.parent; + if (tmp == null) + continue; + + double lat2 = na.getLatitude(tmp.adjNode), lon2 = na.getLongitude(tmp.adjNode); + lastNormedDistance = distanceCalc.calcNormalizedDist(lat1, lon1, lat2, lon2); + if (lastNormedDistance > maxDistance) + break; + } + + // if no path found close to the maxWeight radius then do not return anything! + if (tmpFinishedFrom + && lastNormedDistance > 0 + && lastNormedDistance < distanceCalc.calcNormalizedDist(maxFullDistance / 2 / 4)) + return null; + + return tmp; + } + } +} diff --git a/core/src/main/java/com/graphhopper/routing/RoutingAlgorithm.java b/core/src/main/java/com/graphhopper/routing/RoutingAlgorithm.java index de03219886c..5d6d8705aa0 100644 --- a/core/src/main/java/com/graphhopper/routing/RoutingAlgorithm.java +++ b/core/src/main/java/com/graphhopper/routing/RoutingAlgorithm.java @@ -18,6 +18,7 @@ package com.graphhopper.routing; import com.graphhopper.util.NotThreadSafe; +import java.util.List; /** * Calculates the shortest path from the specified node ids. Can be used only once. @@ -29,11 +30,18 @@ public interface RoutingAlgorithm { /** * Calculates the best path between the specified nodes. - *

+ *

* @return the path. Call the method found() to make sure that the path is valid. */ Path calcPath( int from, int to ); + /** + * Calculates multiple possibilities for a path. + *

+ * @see #calcPath(int, int) + */ + List calcPaths( int from, int to ); + /** * Limits the search to avoid full graph exploration in the case of disconnected networks. The * default value is Double.MAX_VALUE. See #104 diff --git a/core/src/main/java/com/graphhopper/routing/RoutingAlgorithmFactorySimple.java b/core/src/main/java/com/graphhopper/routing/RoutingAlgorithmFactorySimple.java index 920630aceba..eb908195188 100644 --- a/core/src/main/java/com/graphhopper/routing/RoutingAlgorithmFactorySimple.java +++ b/core/src/main/java/com/graphhopper/routing/RoutingAlgorithmFactorySimple.java @@ -54,6 +54,20 @@ public RoutingAlgorithm createAlgo( Graph g, AlgorithmOptions opts ) AStar aStar = new AStar(g, opts.getFlagEncoder(), opts.getWeighting(), opts.getTraversalMode()); aStar.setApproximation(getApproximation(AlgorithmOptions.ASTAR, opts, g.getNodeAccess())); return aStar; + } else if (AlgorithmOptions.ALT_ROUTE.equalsIgnoreCase(algoStr)) + { + AlternativeRoute altRouteAlgo = new AlternativeRoute(g, opts.getFlagEncoder(), opts.getWeighting(), opts.getTraversalMode()); + altRouteAlgo.setMaxPaths(opts.getHints().getInt("alternative_route.max_paths", 2)); + altRouteAlgo.setMaxWeightFactor(opts.getHints().getDouble("alternative_route.max_weight_factor", 1.4)); + altRouteAlgo.setMaxShareFactor(opts.getHints().getDouble("alternative_route.max_share_factor", 0.6)); + altRouteAlgo.setMinPlateauFactor(opts.getHints().getDouble("alternative_route.min_plateau_factor", 0.2)); + altRouteAlgo.setMaxExplorationFactor(opts.getHints().getDouble("alternative_route.max_exploration_factor", 1)); + return altRouteAlgo; + } else if (AlgorithmOptions.ROUND_TRIP_ALT.equalsIgnoreCase(algoStr)) + { + RoundTripAltAlgorithm altRouteAlgo = new RoundTripAltAlgorithm(g, opts.getFlagEncoder(), opts.getWeighting(), opts.getTraversalMode()); + altRouteAlgo.setMaxWeightFactor(opts.getHints().getInt("round_trip_alt.max_weight_factor", 2)); + return altRouteAlgo; } else { throw new IllegalArgumentException("Algorithm " + algoStr + " not found in " + getClass().getName()); diff --git a/core/src/main/java/com/graphhopper/routing/util/BeelineWeightApproximator.java b/core/src/main/java/com/graphhopper/routing/util/BeelineWeightApproximator.java index 3e9a70f0773..65c350786cd 100644 --- a/core/src/main/java/com/graphhopper/routing/util/BeelineWeightApproximator.java +++ b/core/src/main/java/com/graphhopper/routing/util/BeelineWeightApproximator.java @@ -2,7 +2,6 @@ import com.graphhopper.storage.NodeAccess; import com.graphhopper.util.DistanceCalc; -import com.graphhopper.util.DistanceCalcEarth; import com.graphhopper.util.Helper; /** diff --git a/core/src/main/java/com/graphhopper/routing/util/TestAlgoCollector.java b/core/src/main/java/com/graphhopper/routing/util/TestAlgoCollector.java index b34172516f8..3ed3d112c8d 100644 --- a/core/src/main/java/com/graphhopper/routing/util/TestAlgoCollector.java +++ b/core/src/main/java/com/graphhopper/routing/util/TestAlgoCollector.java @@ -17,7 +17,7 @@ */ package com.graphhopper.routing.util; -import com.graphhopper.GHResponse; +import com.graphhopper.AltResponse; import com.graphhopper.routing.*; import com.graphhopper.storage.Graph; import com.graphhopper.storage.CHGraph; @@ -49,7 +49,7 @@ public TestAlgoCollector( String name ) public TestAlgoCollector assertDistance( AlgoHelperEntry algoEntry, List queryList, OneRun oneRun ) { - List viaPaths = new ArrayList(); + List altPaths = new ArrayList(); QueryGraph queryGraph = new QueryGraph(algoEntry.getQueryGraph()); queryGraph.lookup(queryList); AlgorithmOptions opts = algoEntry.opts; @@ -62,15 +62,15 @@ public TestAlgoCollector assertDistance( AlgoHelperEntry algoEntry, List paths, Translation tr ) + public void doWork( AltResponse altRsp, List paths, Translation tr ) { int origPoints = 0; long fullTimeInMillis = 0; @@ -71,9 +71,11 @@ public void doWork( GHResponse rsp, List paths, Translation tr ) InstructionList fullInstructions = new InstructionList(tr); PointList fullPoints = PointList.EMPTY; + List description = new ArrayList(); for (int pathIndex = 0; pathIndex < paths.size(); pathIndex++) { Path path = paths.get(pathIndex); + description.addAll(path.getDescription()); fullTimeInMillis += path.getTime(); fullDistance += path.getDistance(); fullWeight += path.getWeight(); @@ -129,25 +131,26 @@ public void doWork( GHResponse rsp, List paths, Translation tr ) if (!fullPoints.isEmpty()) { - String debug = rsp.getDebugInfo() + ", simplify (" + origPoints + "->" + fullPoints.getSize() + ")"; - rsp.setDebugInfo(debug); + String debug = altRsp.getDebugInfo() + ", simplify (" + origPoints + "->" + fullPoints.getSize() + ")"; + altRsp.addDebugInfo(debug); if (fullPoints.is3D) - calcAscendDescend(rsp, fullPoints); + calcAscendDescend(altRsp, fullPoints); } if (enableInstructions) - rsp.setInstructions(fullInstructions); + altRsp.setInstructions(fullInstructions); if (!allFound) - rsp.addError(new RuntimeException("Connection between locations not found")); + altRsp.addError(new RuntimeException("Connection between locations not found")); - rsp.setPoints(fullPoints). + altRsp.setDescription(description). + setPoints(fullPoints). setRouteWeight(fullWeight). setDistance(fullDistance). setTime(fullTimeInMillis); } - private void calcAscendDescend( final GHResponse rsp, final PointList pointList ) + private void calcAscendDescend( final AltResponse rsp, final PointList pointList ) { double ascendMeters = 0; double descendMeters = 0; diff --git a/core/src/test/java/com/graphhopper/GHResponseTest.java b/core/src/test/java/com/graphhopper/GHResponseTest.java index d303593cd61..5e9b694ba7c 100644 --- a/core/src/test/java/com/graphhopper/GHResponseTest.java +++ b/core/src/test/java/com/graphhopper/GHResponseTest.java @@ -1,11 +1,22 @@ package com.graphhopper; -import junit.framework.TestCase; +import static org.junit.Assert.*; +import org.junit.Test; -public class GHResponseTest extends TestCase +public class GHResponseTest { + @Test public void testToString() throws Exception { - assertEquals("nodes:0; ", new GHResponse().toString()); + assertEquals("no alternatives", new GHResponse().toString()); } -} \ No newline at end of file + + @Test + public void testHasError() throws Exception + { + assertTrue(new GHResponse().hasErrors()); + GHResponse rsp = new GHResponse(); + rsp.addAlternative(new AltResponse()); + assertFalse(rsp.hasErrors()); + } +} diff --git a/core/src/test/java/com/graphhopper/GraphHopperAPITest.java b/core/src/test/java/com/graphhopper/GraphHopperAPITest.java index 0e20dd8cb54..b8be75bef18 100644 --- a/core/src/test/java/com/graphhopper/GraphHopperAPITest.java +++ b/core/src/test/java/com/graphhopper/GraphHopperAPITest.java @@ -19,6 +19,7 @@ import com.graphhopper.routing.util.EncodingManager; import com.graphhopper.storage.*; +import com.graphhopper.util.PointList; import org.junit.Test; import static org.junit.Assert.*; @@ -54,12 +55,15 @@ public void testLoad() loadGraph(graph); GHResponse rsp = instance.route(new GHRequest(42, 10.4, 42, 10)); assertFalse(rsp.hasErrors()); - assertEquals(80, rsp.getDistance(), 1e-6); - assertEquals(42, rsp.getPoints().getLatitude(0), 1e-5); - assertEquals(10.4, rsp.getPoints().getLongitude(0), 1e-5); - assertEquals(41.9, rsp.getPoints().getLatitude(1), 1e-5); - assertEquals(10.2, rsp.getPoints().getLongitude(1), 1e-5); - assertEquals(3, rsp.getPoints().getSize()); + AltResponse arsp = rsp.getFirst(); + assertEquals(80, arsp.getDistance(), 1e-6); + + PointList points = arsp.getPoints(); + assertEquals(42, points.getLatitude(0), 1e-5); + assertEquals(10.4, points.getLongitude(0), 1e-5); + assertEquals(41.9, points.getLatitude(1), 1e-5); + assertEquals(10.2, points.getLongitude(1), 1e-5); + assertEquals(3, points.getSize()); instance.close(); } @@ -86,7 +90,7 @@ public void testDisconnected179() try { - rsp.getPoints(); + rsp.getFirst().getPoints(); assertTrue(false); } catch (Exception ex) { diff --git a/core/src/test/java/com/graphhopper/GraphHopperIT.java b/core/src/test/java/com/graphhopper/GraphHopperIT.java index 621c4e7cacf..2bd61bd7f28 100644 --- a/core/src/test/java/com/graphhopper/GraphHopperIT.java +++ b/core/src/test/java/com/graphhopper/GraphHopperIT.java @@ -88,10 +88,12 @@ public void testMonacoWithInstructions() throws Exception // identify the number of counts to compare with CH foot route assertEquals(698, rsp.getHints().getLong("visited_nodes.sum", 0)); - assertEquals(3437.6, rsp.getDistance(), .1); - assertEquals(89, rsp.getPoints().getSize()); - InstructionList il = rsp.getInstructions(); + AltResponse arsp = rsp.getFirst(); + assertEquals(3437.6, arsp.getDistance(), .1); + assertEquals(89, arsp.getPoints().getSize()); + + InstructionList il = arsp.getInstructions(); assertEquals(13, il.size()); List> resultJson = il.createJson(); @@ -116,13 +118,28 @@ public void testMonacoWithInstructions() throws Exception assertEquals(87, (Long) resultJson.get(4).get("time") / 1000); assertEquals(321, (Long) resultJson.get(5).get("time") / 1000); - List list = rsp.getInstructions().createGPXList(); + List list = arsp.getInstructions().createGPXList(); assertEquals(89, list.size()); final long lastEntryMillis = list.get(list.size() - 1).getTime(); - final long totalResponseMillis = rsp.getTime(); + final long totalResponseMillis = arsp.getTime(); assertEquals(totalResponseMillis, lastEntryMillis); } + @Test + public void testAlternativeRoutes() + { + GHRequest req = new GHRequest(43.729057, 7.41251, 43.740298, 7.423561). + setAlgorithm(AlgorithmOptions.ALT_ROUTE).setVehicle(vehicle).setWeighting(weightCalcStr); + + GHResponse rsp = hopper.route(req); + assertEquals(2, rsp.getAlternatives().size()); + + req.getHints().put("alternative_route.max_paths", "3"); + req.getHints().put("alternative_route.min_plateau_factor", "0.1"); + rsp = hopper.route(req); + assertEquals(3, rsp.getAlternatives().size()); + } + @Test public void testMonacoVia() { @@ -132,10 +149,11 @@ public void testMonacoVia() addPoint(new GHPoint(43.727687, 7.418737)). setAlgorithm(AlgorithmOptions.ASTAR).setVehicle(vehicle).setWeighting(weightCalcStr)); - assertEquals(6875.1, rsp.getDistance(), .1); - assertEquals(179, rsp.getPoints().getSize()); + AltResponse arsp = rsp.getFirst(); + assertEquals(6875.1, arsp.getDistance(), .1); + assertEquals(179, arsp.getPoints().getSize()); - InstructionList il = rsp.getInstructions(); + InstructionList il = arsp.getInstructions(); assertEquals(26, il.size()); List> resultJson = il.createJson(); assertEquals("Continue onto Avenue des Guelfes", resultJson.get(0).get("text")); @@ -172,24 +190,28 @@ public void testMonacoVia() addPoint(new GHPoint(43.727687, 7.418737)). addPoint(new GHPoint(43.727687, 7.418737)). setAlgorithm(AlgorithmOptions.ASTAR).setVehicle(vehicle).setWeighting(weightCalcStr)); - assertEquals(0, rsp.getDistance(), .1); - assertEquals(0, rsp.getRouteWeight(), .1); - assertEquals(1, rsp.getPoints().getSize()); - assertEquals(1, rsp.getInstructions().size()); - assertEquals("Finish!", rsp.getInstructions().createJson().get(0).get("text")); - assertEquals(Instruction.FINISH, rsp.getInstructions().createJson().get(0).get("sign")); + + arsp = rsp.getFirst(); + assertEquals(0, arsp.getDistance(), .1); + assertEquals(0, arsp.getRouteWeight(), .1); + assertEquals(1, arsp.getPoints().getSize()); + assertEquals(1, arsp.getInstructions().size()); + assertEquals("Finish!", arsp.getInstructions().createJson().get(0).get("text")); + assertEquals(Instruction.FINISH, arsp.getInstructions().createJson().get(0).get("sign")); rsp = hopper.route(new GHRequest(). addPoint(new GHPoint(43.727687, 7.418737)). addPoint(new GHPoint(43.727687, 7.418737)). addPoint(new GHPoint(43.727687, 7.418737)). setAlgorithm(AlgorithmOptions.ASTAR).setVehicle(vehicle).setWeighting(weightCalcStr)); - assertEquals(0, rsp.getDistance(), .1); - assertEquals(0, rsp.getRouteWeight(), .1); - assertEquals(2, rsp.getPoints().getSize()); - assertEquals(2, rsp.getInstructions().size()); - assertEquals(Instruction.REACHED_VIA, rsp.getInstructions().createJson().get(0).get("sign")); - assertEquals(Instruction.FINISH, rsp.getInstructions().createJson().get(1).get("sign")); + + arsp = rsp.getFirst(); + assertEquals(0, arsp.getDistance(), .1); + assertEquals(0, arsp.getRouteWeight(), .1); + assertEquals(2, arsp.getPoints().getSize()); + assertEquals(2, arsp.getInstructions().size()); + assertEquals(Instruction.REACHED_VIA, arsp.getInstructions().createJson().get(0).get("sign")); + assertEquals(Instruction.FINISH, arsp.getInstructions().createJson().get(1).get("sign")); } @Test @@ -202,8 +224,9 @@ public void testMonacoEnforcedDirection() req.getHints().put("heading_penalty", "300"); GHResponse rsp = hopper.route(req); - assertEquals(874., rsp.getDistance(), 10.); - assertEquals(33, rsp.getPoints().getSize()); + AltResponse arsp = rsp.getFirst(); + assertEquals(874., arsp.getDistance(), 10.); + assertEquals(33, arsp.getPoints().getSize()); } @Test @@ -217,8 +240,9 @@ public void testMonacoStraightVia() rq.getHints().put("pass_through", true); GHResponse rsp = hopper.route(rq); - assertEquals(297, rsp.getDistance(), 5.); - assertEquals(27, rsp.getPoints().getSize()); + AltResponse arsp = rsp.getFirst(); + assertEquals(297, arsp.getDistance(), 5.); + assertEquals(27, arsp.getPoints().getSize()); } @Test @@ -237,15 +261,16 @@ public void testSRTMWithInstructions() throws Exception GHResponse rsp = tmpHopper.route(new GHRequest(43.730729, 7.421288, 43.727697, 7.419199). setAlgorithm(AlgorithmOptions.ASTAR).setVehicle(vehicle).setWeighting(weightCalcStr)); - assertEquals(1626.8, rsp.getDistance(), .1); - assertEquals(60, rsp.getPoints().getSize()); - assertTrue(rsp.getPoints().is3D()); + AltResponse arsp = rsp.getFirst(); + assertEquals(1626.8, arsp.getDistance(), .1); + assertEquals(60, arsp.getPoints().getSize()); + assertTrue(arsp.getPoints().is3D()); - InstructionList il = rsp.getInstructions(); + InstructionList il = arsp.getInstructions(); assertEquals(10, il.size()); assertTrue(il.get(0).getPoints().is3D()); - String str = rsp.getPoints().toString(); + String str = arsp.getPoints().toString(); assertEquals("(43.73068455771767,7.421283689825812,62.0), (43.73067957305937,7.421382123709815,66.0), " + "(43.73109792316924,7.421546222751131,45.0), (43.73129908884985,7.421589994913116,45.0), " @@ -261,10 +286,10 @@ public void testSRTMWithInstructions() throws Exception + "(43.727680946587874,7.419198768422206,11.0)", str.substring(str.length() - 132)); - assertEquals(84, rsp.getAscend(), 1e-1); - assertEquals(135, rsp.getDescend(), 1e-1); + assertEquals(84, arsp.getAscend(), 1e-1); + assertEquals(135, arsp.getDescend(), 1e-1); - List list = rsp.getInstructions().createGPXList(); + List list = arsp.getInstructions().createGPXList(); assertEquals(60, list.size()); final long lastEntryMillis = list.get(list.size() - 1).getTime(); assertEquals(new GPXEntry(43.73068455771767, 7.421283689825812, 62.0, 0), list.get(0)); @@ -294,10 +319,11 @@ public void testKremsCyclewayInstructionsWithWayTypeInfo() GHResponse rsp = tmpHopper.route(new GHRequest(48.410987, 15.599492, 48.383419, 15.659294). setAlgorithm(AlgorithmOptions.ASTAR).setVehicle(tmpVehicle).setWeighting(tmpWeightCalcStr)); - assertEquals(6932.24, rsp.getDistance(), .1); - assertEquals(110, rsp.getPoints().getSize()); + AltResponse arsp = rsp.getFirst(); + assertEquals(6932.24, arsp.getDistance(), .1); + assertEquals(110, arsp.getPoints().getSize()); - InstructionList il = rsp.getInstructions(); + InstructionList il = arsp.getInstructions(); assertEquals(19, il.size()); List> resultJson = il.createJson(); @@ -345,19 +371,24 @@ public void testRoundaboutInstructionsWithCH() GHResponse rsp = tmpHopper.route(new GHRequest(43.745084, 7.430513, 43.745247, 7.430347) .setVehicle(tmpVehicle).setWeighting(tmpWeightCalcStr)); - assertEquals(2, ((RoundaboutInstruction) rsp.getInstructions().get(1)).getExitNumber()); + + AltResponse arsp = rsp.getFirst(); + assertEquals(2, ((RoundaboutInstruction) arsp.getInstructions().get(1)).getExitNumber()); rsp = tmpHopper.route(new GHRequest(43.745968, 7.42907, 43.745832, 7.428614) .setVehicle(tmpVehicle).setWeighting(tmpWeightCalcStr)); - assertEquals(2, ((RoundaboutInstruction) rsp.getInstructions().get(1)).getExitNumber()); + arsp = rsp.getFirst(); + assertEquals(2, ((RoundaboutInstruction) arsp.getInstructions().get(1)).getExitNumber()); rsp = tmpHopper.route(new GHRequest(43.745948, 7.42914, 43.746173, 7.428834) .setVehicle(tmpVehicle).setWeighting(tmpWeightCalcStr)); - assertEquals(1, ((RoundaboutInstruction) rsp.getInstructions().get(1)).getExitNumber()); + arsp = rsp.getFirst(); + assertEquals(1, ((RoundaboutInstruction) arsp.getInstructions().get(1)).getExitNumber()); rsp = tmpHopper.route(new GHRequest(43.735817, 7.417096, 43.735666, 7.416587) .setVehicle(tmpVehicle).setWeighting(tmpWeightCalcStr)); - assertEquals(2, ((RoundaboutInstruction) rsp.getInstructions().get(1)).getExitNumber()); + arsp = rsp.getFirst(); + assertEquals(2, ((RoundaboutInstruction) arsp.getInstructions().get(1)).getExitNumber()); } @Test @@ -392,15 +423,17 @@ private void checkMultiVehiclesWithCH( GraphHopper tmpHopper ) String str = tmpHopper.getEncodingManager().toString(); GHResponse rsp = tmpHopper.route(new GHRequest(43.73005, 7.415707, 43.741522, 7.42826) .setVehicle("car")); + AltResponse arsp = rsp.getFirst(); assertFalse("car routing for " + str + " should not have errors:" + rsp.getErrors(), rsp.hasErrors()); - assertEquals(207, rsp.getTime() / 1000f, 1); - assertEquals(2838, rsp.getDistance(), 1); + assertEquals(207, arsp.getTime() / 1000f, 1); + assertEquals(2838, arsp.getDistance(), 1); rsp = tmpHopper.route(new GHRequest(43.73005, 7.415707, 43.741522, 7.42826) .setVehicle("bike")); + arsp = rsp.getFirst(); assertFalse("bike routing for " + str + " should not have errors:" + rsp.getErrors(), rsp.hasErrors()); - assertEquals(494, rsp.getTime() / 1000f, 1); - assertEquals(2192, rsp.getDistance(), 1); + assertEquals(494, arsp.getTime() / 1000f, 1); + assertEquals(2192, arsp.getDistance(), 1); rsp = tmpHopper.route(new GHRequest(43.73005, 7.415707, 43.741522, 7.42826) .setVehicle("foot")); @@ -442,12 +475,13 @@ private void executeCHFootRoute() GHResponse rsp = tmpHopper.route(new GHRequest(43.727687, 7.418737, 43.74958, 7.436566). setVehicle(vehicle)); + AltResponse arsp = rsp.getFirst(); // identify the number of counts to compare with none-CH foot route which had nearly 700 counts long sum = rsp.getHints().getLong("visited_nodes.sum", 0); assertNotEquals(sum, 0); assertTrue("Too many nodes visited " + sum, sum < 120); - assertEquals(3437.6, rsp.getDistance(), .1); - assertEquals(89, rsp.getPoints().getSize()); + assertEquals(3437.6, arsp.getDistance(), .1); + assertEquals(89, arsp.getPoints().getSize()); tmpHopper.close(); } } diff --git a/core/src/test/java/com/graphhopper/GraphHopperTest.java b/core/src/test/java/com/graphhopper/GraphHopperTest.java index 7a5ffeeac47..6fd06a6d133 100644 --- a/core/src/test/java/com/graphhopper/GraphHopperTest.java +++ b/core/src/test/java/com/graphhopper/GraphHopperTest.java @@ -78,7 +78,7 @@ public void testLoadOSM() closableInstance.importOrLoad(); GHResponse rsp = closableInstance.route(new GHRequest(51.2492152, 9.4317166, 51.2, 9.4)); assertFalse(rsp.hasErrors()); - assertEquals(3, rsp.getPoints().getSize()); + assertEquals(3, rsp.getFirst().getPoints().getSize()); closableInstance.close(); @@ -87,7 +87,7 @@ public void testLoadOSM() assertTrue(closableInstance.load(ghLoc)); rsp = closableInstance.route(new GHRequest(51.2492152, 9.4317166, 51.2, 9.4)); assertFalse(rsp.hasErrors()); - assertEquals(3, rsp.getPoints().getSize()); + assertEquals(3, rsp.getFirst().getPoints().getSize()); closableInstance.close(); try @@ -120,7 +120,7 @@ public void testLoadOSMNoCH() gh.importOrLoad(); GHResponse rsp = gh.route(new GHRequest(51.2492152, 9.4317166, 51.2, 9.4)); assertFalse(rsp.hasErrors()); - assertEquals(3, rsp.getPoints().getSize()); + assertEquals(3, rsp.getFirst().getPoints().getSize()); gh.close(); gh = new GraphHopper().setStoreOnFlush(true). @@ -129,7 +129,7 @@ public void testLoadOSMNoCH() assertTrue(gh.load(ghLoc)); rsp = gh.route(new GHRequest(51.2492152, 9.4317166, 51.2, 9.4)); assertFalse(rsp.hasErrors()); - assertEquals(3, rsp.getPoints().getSize()); + assertEquals(3, rsp.getFirst().getPoints().getSize()); gh.close(); } @@ -145,7 +145,7 @@ public void testLoadingWithDifferentCHConfig_issue471() gh.importOrLoad(); GHResponse rsp = gh.route(new GHRequest(51.2492152, 9.4317166, 51.2, 9.4)); assertFalse(rsp.hasErrors()); - assertEquals(3, rsp.getPoints().getSize()); + assertEquals(3, rsp.getFirst().getPoints().getSize()); gh.close(); gh = new GraphHopper().setStoreOnFlush(true). @@ -171,7 +171,7 @@ public void testLoadingWithDifferentCHConfig_issue471() gh.importOrLoad(); rsp = gh.route(new GHRequest(51.2492152, 9.4317166, 51.2, 9.4)); assertFalse(rsp.hasErrors()); - assertEquals(3, rsp.getPoints().getSize()); + assertEquals(3, rsp.getFirst().getPoints().getSize()); gh.close(); gh = new GraphHopper().setStoreOnFlush(true). @@ -290,8 +290,8 @@ public void testPrepare() GHResponse rsp = instance.route(new GHRequest(51.2492152, 9.4317166, 51.2, 9.4). setAlgorithm(AlgorithmOptions.DIJKSTRA_BI)); assertFalse(rsp.hasErrors()); - assertEquals(Helper.createPointList(51.249215, 9.431716, 52.0, 9.0, 51.2, 9.4), rsp.getPoints()); - assertEquals(3, rsp.getPoints().getSize()); + assertEquals(Helper.createPointList(51.249215, 9.431716, 52.0, 9.0, 51.2, 9.4), rsp.getFirst().getPoints()); + assertEquals(3, rsp.getFirst().getPoints().getSize()); } @Test @@ -304,8 +304,8 @@ public void testSortedGraph_noCH() setGraphHopperLocation(ghLoc). setOSMFile(testOsm); instance.importOrLoad(); - GHResponse rsp = instance.route(new GHRequest(51.2492152, 9.4317166, 51.2, 9.4). - setAlgorithm(AlgorithmOptions.DIJKSTRA_BI)); + AltResponse rsp = instance.route(new GHRequest(51.2492152, 9.4317166, 51.2, 9.4). + setAlgorithm(AlgorithmOptions.DIJKSTRA_BI)).getFirst(); assertFalse(rsp.hasErrors()); assertEquals(3, rsp.getPoints().getSize()); assertEquals(new GHPoint(51.24921503475044, 9.431716451757769), rsp.getPoints().toGHPoint(0)); @@ -338,9 +338,9 @@ public void testFootAndCar() assertEquals(8, instance.getGraphHopperStorage().getAllEdges().getMaxId()); // A to D - GHResponse rsp = instance.route(new GHRequest(11.1, 50, 11.3, 51).setVehicle(EncodingManager.CAR)); - assertFalse(rsp.hasErrors()); - assertFalse(rsp.hasErrors()); + GHResponse grsp = instance.route(new GHRequest(11.1, 50, 11.3, 51).setVehicle(EncodingManager.CAR)); + assertFalse(grsp.hasErrors()); + AltResponse rsp = grsp.getFirst(); assertEquals(3, rsp.getPoints().getSize()); // => found A and D assertEquals(50, rsp.getPoints().getLongitude(0), 1e-3); @@ -349,21 +349,24 @@ public void testFootAndCar() assertEquals(11.3, rsp.getPoints().getLatitude(2), 1e-3); // A to D not allowed for foot. But the location index will choose a node close to D accessible to FOOT - rsp = instance.route(new GHRequest(11.1, 50, 11.3, 51).setVehicle(EncodingManager.FOOT)); - assertFalse(rsp.hasErrors()); + grsp = instance.route(new GHRequest(11.1, 50, 11.3, 51).setVehicle(EncodingManager.FOOT)); + assertFalse(grsp.hasErrors()); + rsp = grsp.getFirst(); assertEquals(2, rsp.getPoints().getSize()); // => found a point on edge A-B assertEquals(11.680, rsp.getPoints().getLatitude(1), 1e-3); assertEquals(50.644, rsp.getPoints().getLongitude(1), 1e-3); // A to E only for foot - rsp = instance.route(new GHRequest(11.1, 50, 10, 51).setVehicle(EncodingManager.FOOT)); - assertFalse(rsp.hasErrors()); + grsp = instance.route(new GHRequest(11.1, 50, 10, 51).setVehicle(EncodingManager.FOOT)); + assertFalse(grsp.hasErrors()); + rsp = grsp.getFirst(); assertEquals(2, rsp.getPoints().size()); // A D E for car - rsp = instance.route(new GHRequest(11.1, 50, 10, 51).setVehicle(EncodingManager.CAR)); - assertFalse(rsp.hasErrors()); + grsp = instance.route(new GHRequest(11.1, 50, 10, 51).setVehicle(EncodingManager.CAR)); + assertFalse(grsp.hasErrors()); + rsp = grsp.getFirst(); assertEquals(3, rsp.getPoints().getSize()); } @@ -535,9 +538,10 @@ public void testFootOnly() assertEquals(2, instance.getGraphHopperStorage().getAllEdges().getMaxId()); // A to E only for foot - GHResponse res = instance.route(new GHRequest(11.1, 50, 11.2, 52.01).setVehicle(EncodingManager.FOOT)); - assertFalse(res.hasErrors()); - assertEquals(Helper.createPointList(11.1, 50, 10, 51, 11.2, 52), res.getPoints()); + GHResponse grsp = instance.route(new GHRequest(11.1, 50, 11.2, 52.01).setVehicle(EncodingManager.FOOT)); + assertFalse(grsp.hasErrors()); + AltResponse rsp = grsp.getFirst(); + assertEquals(Helper.createPointList(11.1, 50, 10, 51, 11.2, 52), rsp.getPoints()); } @Test @@ -594,16 +598,15 @@ public void testVia() GHPoint third = new GHPoint(11.2, 51.9); GHResponse rsp12 = instance.route(new GHRequest().addPoint(first).addPoint(second)); assertFalse("should find 1->2", rsp12.hasErrors()); - assertEquals(147930.5, rsp12.getDistance(), .1); + assertEquals(147930.5, rsp12.getFirst().getDistance(), .1); GHResponse rsp23 = instance.route(new GHRequest().addPoint(second).addPoint(third)); assertFalse("should find 2->3", rsp23.hasErrors()); - assertEquals(176608.9, rsp23.getDistance(), .1); + assertEquals(176608.9, rsp23.getFirst().getDistance(), .1); - GHResponse rsp = instance.route(new GHRequest().addPoint(first).addPoint(second).addPoint(third)); - - assertFalse(rsp.hasErrors()); - assertFalse("should find 1->2->3", rsp.hasErrors()); - assertEquals(rsp12.getDistance() + rsp23.getDistance(), rsp.getDistance(), 1e-6); + GHResponse grsp = instance.route(new GHRequest().addPoint(first).addPoint(second).addPoint(third)); + assertFalse("should find 1->2->3", grsp.hasErrors()); + AltResponse rsp = grsp.getFirst(); + assertEquals(rsp12.getFirst().getDistance() + rsp23.getFirst().getDistance(), rsp.getDistance(), 1e-6); assertEquals(5, rsp.getPoints().getSize()); assertEquals(5, rsp.getInstructions().size()); assertEquals(Instruction.REACHED_VIA, rsp.getInstructions().get(1).getSign()); @@ -624,7 +627,8 @@ public void testGetPathsDirectionEnforcement1() // Test enforce south start direction; expected nodes (9)-5-8-3-(10) GHRequest req = new GHRequest().addPoint(start, 180.).addPoint(end); GHResponse response = new GHResponse(); - List paths = instance.getPaths(req, response); + List paths = instance.calcPaths(req, response); + assertFalse(response.hasErrors()); assertArrayEquals(new int[] { 9, 5, 8, 3, 10 @@ -645,7 +649,8 @@ public void testGetPathsDirectionEnforcement2() // Test enforce south start direction and east end direction ; expected nodes (9)-5-8-1-2-(10) GHRequest req = new GHRequest().addPoint(start, 180.).addPoint(end, 90.); GHResponse response = new GHResponse(); - List paths = instance.getPaths(req, response); + List paths = instance.calcPaths(req, response); + assertFalse(response.hasErrors()); assertArrayEquals(new int[] { 9, 5, 8, 1, 2, 10 @@ -654,7 +659,8 @@ public void testGetPathsDirectionEnforcement2() // Test uni-directional case req.setAlgorithm(AlgorithmOptions.DIJKSTRA); response = new GHResponse(); - paths = instance.getPaths(req, response); + paths = instance.calcPaths(req, response); + assertFalse(response.hasErrors()); assertArrayEquals(new int[] { 9, 5, 8, 1, 2, 10 @@ -675,7 +681,8 @@ public void testGetPathsDirectionEnforcement3() GHRequest req = new GHRequest().addPoint(start).addPoint(via, 0.).addPoint(end); GHResponse response = new GHResponse(); - List paths = instance.getPaths(req, response); + List paths = instance.calcPaths(req, response); + assertFalse(response.hasErrors()); assertArrayEquals(new int[] { 10, 5, 6, 7, 11 @@ -697,7 +704,9 @@ public void testGetPathsDirectionEnforcement4() GHRequest req = new GHRequest().addPoint(start).addPoint(via).addPoint(end); req.getHints().put("pass_through", true); GHResponse response = new GHResponse(); - List paths = instance.getPaths(req, response); + List paths = instance.calcPaths(req, response); + assertFalse(response.hasErrors()); + assertEquals(1, response.getAlternatives().size()); assertArrayEquals(new int[] { 10, 4, 3, 11 @@ -723,7 +732,8 @@ public void testGetPathsDirectionEnforcement5() GHRequest req = new GHRequest().addPoint(start, 0.).addPoint(via, 3.14 / 2).addPoint(end); req.getHints().put("pass_through", true); GHResponse response = new GHResponse(); - List paths = instance.getPaths(req, response); + List paths = instance.calcPaths(req, response); + assertFalse(response.hasErrors()); assertArrayEquals(new int[] { 10, 4, 3, 8, 7, 9 @@ -747,7 +757,8 @@ public void testGetPathsDirectionEnforcement6() GHRequest req = new GHRequest().addPoint(start, 90.).addPoint(via, 270.).addPoint(end, 270.); GHResponse response = new GHResponse(); - List paths = instance.getPaths(req, response); + List paths = instance.calcPaths(req, response); + assertFalse(response.hasErrors()); assertArrayEquals(new int[] { 0, 1, 2 @@ -825,7 +836,7 @@ public void testCustomFactoryForNoneCH() assertTrue(af == instance.getAlgorithmFactory(weighting)); - // test that hints are passwed to algorithm opts + // test that hints are passed to algorithm opts final AtomicInteger cnt = new AtomicInteger(0); instance.putAlgorithmFactory(weighting, new RoutingAlgorithmFactorySimple() { diff --git a/core/src/test/java/com/graphhopper/routing/AlternativeRouteTest.java b/core/src/test/java/com/graphhopper/routing/AlternativeRouteTest.java new file mode 100644 index 00000000000..af22788821a --- /dev/null +++ b/core/src/test/java/com/graphhopper/routing/AlternativeRouteTest.java @@ -0,0 +1,185 @@ +package com.graphhopper.routing; + +import com.graphhopper.routing.util.CarFlagEncoder; +import com.graphhopper.routing.util.EncodingManager; +import com.graphhopper.routing.util.FastestWeighting; +import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.routing.util.TraversalMode; +import com.graphhopper.routing.util.Weighting; +import com.graphhopper.storage.Graph; +import com.graphhopper.storage.GraphHopperStorage; +import com.graphhopper.storage.RAMDirectory; +import com.graphhopper.util.Helper; + +import org.junit.Test; + +import java.util.List; + +import static com.graphhopper.routing.AbstractRoutingAlgorithmTester.updateDistancesFor; +import com.graphhopper.routing.AlternativeRoute.AltDijkstraBidirectionRef; +import com.graphhopper.storage.*; +import java.util.Arrays; +import java.util.Collection; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class AlternativeRouteTest +{ + private final FlagEncoder carFE = new CarFlagEncoder(); + private final EncodingManager em = new EncodingManager(carFE); + private final TraversalMode traversalMode; + + /** + * Runs the same test with each of the supported traversal modes + */ + @Parameterized.Parameters(name = "{0}") + public static Collection configs() + { + return Arrays.asList(new Object[][] + { + { + TraversalMode.NODE_BASED + }, + { + TraversalMode.EDGE_BASED_2DIR + } + }); + } + + public AlternativeRouteTest( TraversalMode tMode ) + { + this.traversalMode = tMode; + } + + GraphHopperStorage createTestGraph( boolean fullGraph, EncodingManager tmpEM ) + { + GraphHopperStorage graph = new GraphHopperStorage(new RAMDirectory(), tmpEM, false, new GraphExtension.NoOpExtension()); + graph.create(1000); + + /* 9 + _/\ + 1 2-3-4-10 + \ / \ + 5--6-7---8 + + */ + graph.edge(1, 9, 1, true); + graph.edge(9, 2, 1, true); + if (fullGraph) + graph.edge(2, 3, 1, true); + graph.edge(3, 4, 1, true); + graph.edge(4, 10, 1, true); + + graph.edge(5, 6, 1, true); + + graph.edge(6, 7, 1, true); + graph.edge(7, 8, 1, true); + + if (fullGraph) + graph.edge(1, 5, 2, true); + graph.edge(6, 3, 1, true); + graph.edge(4, 8, 1, true); + + updateDistancesFor(graph, 5, 0.00, 0.05); + updateDistancesFor(graph, 6, 0.00, 0.10); + updateDistancesFor(graph, 7, 0.00, 0.15); + updateDistancesFor(graph, 8, 0.00, 0.25); + + updateDistancesFor(graph, 1, 0.05, 0.00); + updateDistancesFor(graph, 9, 0.10, 0.05); + updateDistancesFor(graph, 2, 0.05, 0.10); + updateDistancesFor(graph, 3, 0.05, 0.15); + updateDistancesFor(graph, 4, 0.05, 0.25); + updateDistancesFor(graph, 10, 0.05, 0.30); + return graph; + } + + @Test + public void testCalcAlternatives() throws Exception + { + Weighting weighting = new FastestWeighting(carFE); + GraphHopperStorage g = createTestGraph(true, em); + AlternativeRoute altDijkstra = new AlternativeRoute(g, carFE, weighting, traversalMode); + altDijkstra.setMaxShareFactor(0.5); + altDijkstra.setMaxWeightFactor(2); + List pathInfos = altDijkstra.calcAlternatives(5, 4); + checkAlternatives(pathInfos); + assertEquals(2, pathInfos.size()); + + DijkstraBidirectionRef dijkstra = new DijkstraBidirectionRef(g, carFE, weighting, traversalMode); + Path bestPath = dijkstra.calcPath(5, 4); + + Path bestAlt = pathInfos.get(0).getPath(); + Path secondAlt = pathInfos.get(1).getPath(); + + assertEquals(bestPath.calcNodes(), bestAlt.calcNodes()); + assertEquals(bestPath.getWeight(), bestAlt.getWeight(), 1e-3); + + assertEquals(Helper.createTList(5, 6, 3, 4), bestAlt.calcNodes()); + + // Note: here plateau is longer, even longer than optimum, but path is longer + // so which alternative is better? longer plateau.weight with bigger path.weight or smaller path.weight with smaller plateau.weight + // assertEquals(Helper.createTList(5, 1, 9, 2, 3, 4), secondAlt.calcNodes()); + assertEquals(Helper.createTList(5, 6, 7, 8, 4), secondAlt.calcNodes()); + assertEquals(1667.9, secondAlt.getWeight(), .1); + } + + @Test + public void testCalcAlternatives2() throws Exception + { + Weighting weighting = new FastestWeighting(carFE); + Graph g = createTestGraph(true, em); + AlternativeRoute altDijkstra = new AlternativeRoute(g, carFE, weighting, traversalMode); + altDijkstra.setMaxPaths(3); + altDijkstra.setMaxShareFactor(0.7); + altDijkstra.setMinPlateauFactor(0.15); + altDijkstra.setMaxWeightFactor(2); + // edge based traversal requires a bit more exploration than the default of 1 + altDijkstra.setMaxExplorationFactor(1.2); + + List pathInfos = altDijkstra.calcAlternatives(5, 4); + checkAlternatives(pathInfos); + assertEquals(3, pathInfos.size()); + + // result is sorted based on the plateau to full weight ratio + assertEquals(Helper.createTList(5, 6, 3, 4), pathInfos.get(0).getPath().calcNodes()); + assertEquals(Helper.createTList(5, 6, 7, 8, 4), pathInfos.get(1).getPath().calcNodes()); + assertEquals(Helper.createTList(5, 1, 9, 2, 3, 4), pathInfos.get(2).getPath().calcNodes()); + assertEquals(2416.0, pathInfos.get(2).getPath().getWeight(), .1); + } + + void checkAlternatives( List alternativeInfos ) + { + assertFalse("alternativeInfos should contain alternatives", alternativeInfos.isEmpty()); + AlternativeRoute.AlternativeInfo bestInfo = alternativeInfos.get(0); + for (int i = 1; i < alternativeInfos.size(); i++) + { + AlternativeRoute.AlternativeInfo a = alternativeInfos.get(i); + if (a.getPath().getWeight() < bestInfo.getPath().getWeight()) + assertTrue("alternative is not longer -> " + a + " vs " + bestInfo, false); + + if (a.getShareWeight() > bestInfo.getPath().getWeight() + || a.getShareWeight() > a.getPath().getWeight()) + assertTrue("share or sortby incorrect -> " + a + " vs " + bestInfo, false); + } + } + + @Test + public void testDisconnectedAreas() + { + Graph g = createTestGraph(true, em); + + // one single disconnected node + updateDistancesFor(g, 20, 0.00, -0.01); + + Weighting weighting = new FastestWeighting(carFE); + AltDijkstraBidirectionRef altDijkstra = new AltDijkstraBidirectionRef(g, carFE, weighting, traversalMode, 1); + Path path = altDijkstra.calcPath(1, 20); + assertFalse(path.isFound()); + + // make sure not the full graph is traversed! + assertEquals(3, altDijkstra.getVisitedNodes()); + } +} diff --git a/core/src/test/java/com/graphhopper/routing/RoundTripAltAlgorithmTest.java b/core/src/test/java/com/graphhopper/routing/RoundTripAltAlgorithmTest.java new file mode 100644 index 00000000000..de93a607d1f --- /dev/null +++ b/core/src/test/java/com/graphhopper/routing/RoundTripAltAlgorithmTest.java @@ -0,0 +1,103 @@ +/* + * Licensed to GraphHopper and Peter Karich under one or more contributor + * license agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * GraphHopper 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.*; +import com.graphhopper.storage.Graph; +import com.graphhopper.util.Helper; +import java.util.List; +import org.junit.*; +import static org.junit.Assert.*; + +/** + * + * @author Peter Karich + */ +public class RoundTripAltAlgorithmTest +{ + private final FlagEncoder carFE = new CarFlagEncoder(); + private final EncodingManager em = new EncodingManager(carFE); + // TODO private final TraversalMode tMode = TraversalMode.EDGE_BASED_2DIR; + private final TraversalMode tMode = TraversalMode.NODE_BASED; + + @Test + public void testCalcRoundTrip() throws Exception + { + Weighting weighting = new FastestWeighting(carFE); + Graph g = createTestGraph(true); + RoundTripAltAlgorithm rtAlgo = new RoundTripAltAlgorithm(g, carFE, weighting, tMode); + double maxDist = Helper.DIST_EARTH.calcDist(0, 0, 0.05, 0.25) * 2; + rtAlgo.setMaxWeightFactor(2); + List paths = rtAlgo.calcRoundTrips(5, maxDist, 1.2); + assertEquals(2, paths.size()); + assertEquals(Helper.createTList(5, 6, 3, 4), paths.get(0).calcNodes()); + assertEquals(Helper.createTList(4, 8, 7, 6, 5), paths.get(1).calcNodes()); + + rtAlgo = new RoundTripAltAlgorithm(g, carFE, weighting, tMode); + rtAlgo.setMaxWeightFactor(2); + paths = rtAlgo.calcRoundTrips(6, maxDist, 2); + assertEquals(2, paths.size()); + assertEquals(Helper.createTList(6, 3, 4), paths.get(0).calcNodes()); + assertEquals(Helper.createTList(4, 8, 7, 6), paths.get(1).calcNodes()); + + rtAlgo = new RoundTripAltAlgorithm(g, carFE, weighting, tMode); + rtAlgo.setMaxWeightFactor(2); + paths = rtAlgo.calcRoundTrips(6, maxDist, 1); + assertEquals(2, paths.size()); + assertEquals(Helper.createTList(6, 3, 4), paths.get(0).calcNodes()); + assertEquals(Helper.createTList(4, 3, 6), paths.get(1).calcNodes()); + } + + // TODO how to select alternative when the second best is the 'bestForwardPath' backwards? + @Ignore + public void testCalcRoundTripWithBiggerPenalty() throws Exception + { + Weighting weighting = new FastestWeighting(carFE); + Graph g = createTestGraph(true); + double maxDist = Helper.DIST_EARTH.calcDist(0, 0, 0.05, 0.25) * 2; + RoundTripAltAlgorithm rtAlgo = new RoundTripAltAlgorithm(g, carFE, weighting, tMode); + rtAlgo.setMaxWeightFactor(2); + List paths = rtAlgo.calcRoundTrips(6, maxDist, 2); + assertEquals(2, paths.size()); + // here we get 6,3,4,10 as best forward and 10,4,8,7,6 as best backward but 10,4,3,6 is selected as it looks like the 'alternative' + assertEquals(Helper.createTList(6, 3, 4), paths.get(0).calcNodes()); + assertEquals(Helper.createTList(4, 8, 7, 6), paths.get(1).calcNodes()); + } + + @Test + public void testCalcRoundTripWhereAlreadyPlateauStartIsDifferentToBestRoute() throws Exception + { + // exception occured for 51.074194,13.705444 + Weighting weighting = new FastestWeighting(carFE); + + // now force that start of plateau of alternative is already different edge than optimal route + Graph g = createTestGraph(false); + RoundTripAltAlgorithm rtAlgo = new RoundTripAltAlgorithm(g, carFE, weighting, tMode); + rtAlgo.setMaxWeightFactor(2); + double maxDist = Helper.DIST_EARTH.calcDist(0, 0, 0.05, 0.25) * 2; + List paths = rtAlgo.calcRoundTrips(5, maxDist, 1.4); + assertEquals(2, paths.size()); + assertEquals(Helper.createTList(5, 6, 3, 4), paths.get(0).calcNodes()); + assertEquals(Helper.createTList(4, 8, 7, 6, 5), paths.get(1).calcNodes()); + } + + private Graph createTestGraph( boolean b ) + { + return new AlternativeRouteTest(tMode).createTestGraph(b, em); + } +} diff --git a/tools/src/main/java/com/graphhopper/tools/Measurement.java b/tools/src/main/java/com/graphhopper/tools/Measurement.java index adbb038ad46..bffc3387457 100644 --- a/tools/src/main/java/com/graphhopper/tools/Measurement.java +++ b/tools/src/main/java/com/graphhopper/tools/Measurement.java @@ -17,6 +17,7 @@ */ package com.graphhopper.tools; +import com.graphhopper.AltResponse; import com.graphhopper.GHRequest; import com.graphhopper.GHResponse; import com.graphhopper.GraphHopper; @@ -344,10 +345,10 @@ public int doCalc( boolean warmup, int run ) // req.getHints().put(algo + ".approximation", "BeelineSimplification"); // req.getHints().put(algo + ".epsilon", 2); req.getHints().put("instructions", withInstructions); - GHResponse res; + GHResponse rsp; try { - res = hopper.route(req); + rsp = hopper.route(req); } catch (Exception ex) { // 'not found' can happen if import creates more than one subnetwork @@ -355,21 +356,22 @@ public int doCalc( boolean warmup, int run ) + "nodes:" + from + " -> " + to + ", request:" + req, ex); } - if (res.hasErrors()) + if (rsp.hasErrors()) { if (!warmup) failedCount.incrementAndGet(); - if (!res.getErrors().get(0).getMessage().toLowerCase().contains("not found")) - logger.error("errors should NOT happen in Measurement! " + req + " => " + res.getErrors()); + if (!rsp.getErrors().get(0).getMessage().toLowerCase().contains("not found")) + logger.error("errors should NOT happen in Measurement! " + req + " => " + rsp.getErrors()); return 0; } + AltResponse arsp = rsp.getFirst(); if (!warmup) { - visitedNodesSum.addAndGet(res.getHints().getLong("visited_nodes.sum", 0)); - long dist = (long) res.getDistance(); + visitedNodesSum.addAndGet(rsp.getHints().getLong("visited_nodes.sum", 0)); + long dist = (long) arsp.getDistance(); distSum.addAndGet(dist); airDistSum.addAndGet((long) distCalc.calcDist(fromLat, fromLon, toLat, toLon)); @@ -386,7 +388,7 @@ public int doCalc( boolean warmup, int run ) // calcPointsTimeSum.addAndGet(System.nanoTime() - start); } - return res.getPoints().getSize(); + return arsp.getPoints().getSize(); } }.setIterations(count).start(); diff --git a/tools/src/main/java/com/graphhopper/ui/MiniGraphUI.java b/tools/src/main/java/com/graphhopper/ui/MiniGraphUI.java index 48f0e7d53db..fbf6ec3a183 100644 --- a/tools/src/main/java/com/graphhopper/ui/MiniGraphUI.java +++ b/tools/src/main/java/com/graphhopper/ui/MiniGraphUI.java @@ -22,6 +22,7 @@ import com.graphhopper.coll.GHTBitSet; import com.graphhopper.routing.*; import com.graphhopper.routing.util.*; +import com.graphhopper.storage.EdgeEntry; import com.graphhopper.storage.Graph; import com.graphhopper.storage.NodeAccess; import com.graphhopper.storage.index.LocationIndexTree; @@ -29,6 +30,7 @@ import com.graphhopper.util.*; import com.graphhopper.util.shapes.BBox; import gnu.trove.list.TIntList; +import gnu.trove.map.TIntObjectMap; import java.awt.*; import java.awt.event.*; @@ -40,10 +42,8 @@ /** * A rough graphical user interface for visualizing the OSM graph. Mainly for debugging algorithms - * and spatial datastructures. - *

- * Use the project at https://github.com/graphhopper/graphhopper-web for a - * better/faster/userfriendly/... alternative! + * and spatial datastructures. Use the 'web' module for a more userfriendly UI as shown at + * graphhopper.com/maps *

* @author Peter Karich */ @@ -533,4 +533,53 @@ void repaintRoads() mainPanel.repaint(); logger.info("roads painting took " + sw.stop().getSeconds() + " sec"); } + + static class MyBiDi extends DijkstraBidirectionRef + { + + public MyBiDi( Graph graph, FlagEncoder encoder, Weighting weighting, TraversalMode tMode ) + { + super(graph, encoder, weighting, tMode); + } + + @Override + public boolean finished() + { + // we need to finish BOTH searches identical to CH + if (finishedFrom && finishedTo) + return true; + + if (currFrom.weight + currTo.weight > weightLimit) + return true; + + // The following condition is necessary to avoid traversing the full graph if areas are disconnected + // but it is only valid for none-CH e.g. for CH it can happen that finishedTo is true but the from-SPT could still reach 'to' + if (!bestPath.isFound() && (finishedFrom || finishedTo)) + return true; + + return currFrom.weight > bestPath.getWeight() && currTo.weight > bestPath.getWeight(); + } + + public TIntObjectMap getBestWeightMapFrom() + { + return bestWeightMapFrom; + } + + public TIntObjectMap getBestWeightMapTo() + { + return bestWeightMapTo; + } + + @Override + protected double getCurrentFromWeight() + { + return super.getCurrentFromWeight(); + } + + @Override + protected double getCurrentToWeight() + { + return super.getCurrentToWeight(); + } + } } diff --git a/web/src/main/java/com/graphhopper/http/GraphHopperServlet.java b/web/src/main/java/com/graphhopper/http/GraphHopperServlet.java index b11b4b9662b..2675aa83831 100644 --- a/web/src/main/java/com/graphhopper/http/GraphHopperServlet.java +++ b/web/src/main/java/com/graphhopper/http/GraphHopperServlet.java @@ -17,6 +17,7 @@ */ package com.graphhopper.http; +import com.graphhopper.AltResponse; import com.graphhopper.GHRequest; import com.graphhopper.GHResponse; import com.graphhopper.GraphHopper; @@ -89,21 +90,23 @@ public void doGet( HttpServletRequest httpReq, HttpServletResponse httpRes ) thr } StopWatch sw = new StopWatch().start(); + List errorList = new ArrayList(); if (!hopper.getEncodingManager().supports(vehicleStr)) { - ghRsp.addError(new IllegalArgumentException("Vehicle not supported: " + vehicleStr)); + errorList.add(new IllegalArgumentException("Vehicle not supported: " + vehicleStr)); } else if (enableElevation && !hopper.hasElevation()) { - ghRsp.addError(new IllegalArgumentException("Elevation not supported!")); + errorList.add(new IllegalArgumentException("Elevation not supported!")); } else if (favoredHeadings.size() > 1 && favoredHeadings.size() != requestPoints.size()) { - ghRsp.addError(new IllegalArgumentException("The number of 'heading' parameters must be <= 1 " + errorList.add(new IllegalArgumentException("The number of 'heading' parameters must be <= 1 " + "or equal to the number of points (" + requestPoints.size() + ")")); } - if (!ghRsp.hasErrors()) + + ghRsp.addErrors(errorList); + if (errorList.isEmpty()) { FlagEncoder algoVehicle = hopper.getEncodingManager().getEncoder(vehicleStr); - GHRequest request; if (favoredHeadings.size() > 0) { @@ -142,22 +145,33 @@ public void doGet( HttpServletRequest httpReq, HttpServletResponse httpRes ) thr + took + ", " + algoStr + ", " + weighting + ", " + vehicleStr; httpRes.setHeader("X-GH-Took", "" + Math.round(took * 1000)); + int alternatives = ghRsp.getAlternatives().size(); + if (writeGPX && alternatives > 1) + ghRsp.addError(new IllegalAccessException("Alternatives are currently not supported for GPX")); + if (ghRsp.hasErrors()) + { logger.error(logStr + ", errors:" + ghRsp.getErrors()); - else - logger.info(logStr + ", distance: " + ghRsp.getDistance() - + ", time:" + Math.round(ghRsp.getTime() / 60000f) - + "min, points:" + ghRsp.getPoints().getSize() + ", debug - " + ghRsp.getDebugInfo()); + } else + { + AltResponse altRsp0 = ghRsp.getFirst(); + logger.info(logStr + ", alternatives: " + alternatives + + ", distance0: " + altRsp0.getDistance() + + ", time0: " + Math.round(altRsp0.getTime() / 60000f) + "min" + + ", points0: " + altRsp0.getPoints().getSize() + + ", debugInfo: " + ghRsp.getDebugInfo()); + } if (writeGPX) { - String xml = createGPXString(httpReq, httpRes, ghRsp); if (ghRsp.hasErrors()) { httpRes.setStatus(SC_BAD_REQUEST); - httpRes.getWriter().append(xml); + httpRes.getWriter().append(errorsToXML(ghRsp.getErrors())); } else { + // no error => we can now safely call getFirst + String xml = createGPXString(httpReq, httpRes, ghRsp.getFirst()); writeResponse(httpRes, xml); } } else @@ -177,7 +191,7 @@ public void doGet( HttpServletRequest httpReq, HttpServletResponse httpRes ) thr } } - protected String createGPXString( HttpServletRequest req, HttpServletResponse res, GHResponse rsp ) + protected String createGPXString( HttpServletRequest req, HttpServletResponse res, AltResponse rsp ) { boolean includeElevation = getBooleanParam(req, "elevation", false); // default to false for the route part in next API version, see #437 @@ -189,14 +203,14 @@ protected String createGPXString( HttpServletRequest req, HttpServletResponse re String trackName = getParam(req, "trackname", "GraphHopper Track"); res.setHeader("Content-Disposition", "attachment;filename=" + "GraphHopper.gpx"); long time = getLongParam(req, "millis", System.currentTimeMillis()); - if (rsp.hasErrors()) - return errorsToXML(rsp.getErrors()); - else - return rsp.getInstructions().createGPX(trackName, time, includeElevation, withRoute, withTrack, withWayPoints); + return rsp.getInstructions().createGPX(trackName, time, includeElevation, withRoute, withTrack, withWayPoints); } protected String errorsToXML( List list ) { + if (list.isEmpty()) + throw new RuntimeException("errorsToXML should not be called with an empty list"); + try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); diff --git a/web/src/main/java/com/graphhopper/http/GraphHopperWeb.java b/web/src/main/java/com/graphhopper/http/GraphHopperWeb.java index 9adbf0d577d..ff67a02cf98 100644 --- a/web/src/main/java/com/graphhopper/http/GraphHopperWeb.java +++ b/web/src/main/java/com/graphhopper/http/GraphHopperWeb.java @@ -17,11 +17,13 @@ */ package com.graphhopper.http; +import com.graphhopper.AltResponse; import com.graphhopper.GHRequest; import com.graphhopper.GHResponse; import com.graphhopper.GraphHopperAPI; import com.graphhopper.util.*; import com.graphhopper.util.shapes.GHPoint; +import java.util.ArrayList; import java.util.List; @@ -91,7 +93,6 @@ public GraphHopperWeb setElevation( boolean withElevation ) @Override public GHResponse route( GHRequest request ) { - StopWatch sw = new StopWatch().start(); try { String places = ""; @@ -117,7 +118,7 @@ public GHResponse route( GHRequest request ) + "&instructions=" + tmpInstructions + "&points_encoded=true" + "&calc_points=" + tmpCalcPoints - + "&algo=" + request.getAlgorithm() + + "&algorithm=" + request.getAlgorithm() + "&locale=" + request.getLocale().toString() + "&elevation=" + tmpElevation; @@ -126,18 +127,26 @@ public GHResponse route( GHRequest request ) if (!tmpKey.isEmpty()) url += "&key=" + tmpKey; + int altMax = request.getHints().getInt("alternative_route.max_num", 0); + if(altMax!=0){ + url += "&alternative_route.max_num=" + altMax; + } String str = downloader.downloadAsString(url, true); JSONObject json = new JSONObject(str); GHResponse res = new GHResponse(); - readErrors(res.getErrors(), json); - if (res.hasErrors()) + res.addErrors(readErrors(json)); + if (res.hasRawErrors()) return res; - + JSONArray paths = json.getJSONArray("paths"); - JSONObject firstPath = paths.getJSONObject(0); - readPath(res, firstPath, tmpCalcPoints, tmpInstructions, tmpElevation); + for (int index = 0; index < paths.length(); index++) + { + JSONObject path = paths.getJSONObject(index); + AltResponse altRsp = createAltResponse(path, tmpCalcPoints, tmpInstructions, tmpElevation); + res.addAlternative(altRsp); + } return res; } catch (Exception ex) @@ -146,22 +155,25 @@ public GHResponse route( GHRequest request ) } } - public static void readPath( GHResponse res, JSONObject firstPath, - boolean tmpCalcPoints, - boolean tmpInstructions, - boolean tmpElevation ) + public static AltResponse createAltResponse( JSONObject path, + boolean tmpCalcPoints, boolean tmpInstructions, boolean tmpElevation ) { - double distance = firstPath.getDouble("distance"); - long time = firstPath.getLong("time"); + AltResponse altRsp = new AltResponse(); + altRsp.addErrors(readErrors(path)); + if (altRsp.hasErrors()) + return altRsp; + + double distance = path.getDouble("distance"); + long time = path.getLong("time"); if (tmpCalcPoints) { - String pointStr = firstPath.getString("points"); + String pointStr = path.getString("points"); PointList pointList = WebHelper.decodePolyline(pointStr, 100, tmpElevation); - res.setPoints(pointList); + altRsp.setPoints(pointList); if (tmpInstructions) { - JSONArray instrArr = firstPath.getJSONArray("instructions"); + JSONArray instrArr = path.getJSONArray("instructions"); InstructionList il = new InstructionList(null); int viaCount = 1; @@ -212,14 +224,16 @@ public static void readPath( GHResponse res, JSONObject firstPath, instr.setDistance(instDist).setTime(instTime); il.add(instr); } - res.setInstructions(il); + altRsp.setInstructions(il); } } - res.setDistance(distance).setTime(time); + altRsp.setDistance(distance).setTime(time); + return altRsp; } - public static void readErrors( List errors, JSONObject json ) + public static List readErrors( JSONObject json ) { + List errors = new ArrayList(); JSONArray errorJson; if (json.has("message")) @@ -231,7 +245,7 @@ public static void readErrors( List errors, JSONObject json ) { // should not happen errors.add(new RuntimeException(json.getString("message"))); - return; + return errors; } } else if (json.has("info")) { @@ -240,10 +254,10 @@ public static void readErrors( List errors, JSONObject json ) if (jsonInfo.has("errors")) errorJson = jsonInfo.getJSONArray("errors"); else - return; + return errors; } else - return; + return errors; for (int i = 0; i < errorJson.length(); i++) { @@ -270,5 +284,7 @@ else if (exClass.isEmpty()) if (json.has("message") && errors.isEmpty()) errors.add(new RuntimeException(json.getString("message"))); + + return errors; } } diff --git a/web/src/main/java/com/graphhopper/http/SimpleRouteSerializer.java b/web/src/main/java/com/graphhopper/http/SimpleRouteSerializer.java index 362c07bda25..66098063db2 100644 --- a/web/src/main/java/com/graphhopper/http/SimpleRouteSerializer.java +++ b/web/src/main/java/com/graphhopper/http/SimpleRouteSerializer.java @@ -17,6 +17,7 @@ */ package com.graphhopper.http; +import com.graphhopper.AltResponse; import com.graphhopper.GHResponse; import com.graphhopper.util.Helper; import com.graphhopper.util.InstructionList; @@ -62,34 +63,43 @@ public Map toJSON( GHResponse rsp, json.put("info", jsonInfo); json.put("hints", rsp.getHints().toMap()); jsonInfo.put("copyrights", Arrays.asList("GraphHopper", "OpenStreetMap contributors")); - Map jsonPath = new HashMap(); - jsonPath.put("distance", Helper.round(rsp.getDistance(), 3)); - jsonPath.put("weight", Helper.round6(rsp.getDistance())); - jsonPath.put("time", rsp.getTime()); - if (calcPoints) + List> jsonPathList = new ArrayList>(); + for (AltResponse ar : rsp.getAlternatives()) { - jsonPath.put("points_encoded", pointsEncoded); + Map jsonPath = new HashMap(); + jsonPath.put("distance", Helper.round(ar.getDistance(), 3)); + jsonPath.put("weight", Helper.round6(ar.getRouteWeight())); + jsonPath.put("time", ar.getTime()); + if (!ar.getDescription().isEmpty()) + jsonPath.put("description", ar.getDescription()); - PointList points = rsp.getPoints(); - if (points.getSize() >= 2) + if (calcPoints) { - BBox maxBounds2D = new BBox(maxBounds.minLon, maxBounds.maxLon, maxBounds.minLat, maxBounds.maxLat); - jsonPath.put("bbox", rsp.calcRouteBBox(maxBounds2D).toGeoJson()); - } + jsonPath.put("points_encoded", pointsEncoded); - jsonPath.put("points", createPoints(points, pointsEncoded, includeElevation)); + PointList points = ar.getPoints(); + if (points.getSize() >= 2) + { + BBox maxBounds2D = new BBox(maxBounds.minLon, maxBounds.maxLon, maxBounds.minLat, maxBounds.maxLat); + jsonPath.put("bbox", ar.calcRouteBBox(maxBounds2D).toGeoJson()); + } - if (enableInstructions) - { - InstructionList instructions = rsp.getInstructions(); - jsonPath.put("instructions", instructions.createJson()); + jsonPath.put("points", createPoints(points, pointsEncoded, includeElevation)); + + if (enableInstructions) + { + InstructionList instructions = ar.getInstructions(); + jsonPath.put("instructions", instructions.createJson()); + } + + jsonPath.put("ascend", ar.getAscend()); + jsonPath.put("descend", ar.getDescend()); } - jsonPath.put("ascend", rsp.getAscend()); - jsonPath.put("descend", rsp.getDescend()); + jsonPathList.add(jsonPath); } - json.put("paths", Collections.singletonList(jsonPath)); + json.put("paths", jsonPathList); } return json; } diff --git a/web/src/main/webapp/css/style.css b/web/src/main/webapp/css/style.css index 070578dcf9a..3c152275094 100644 --- a/web/src/main/webapp/css/style.css +++ b/web/src/main/webapp/css/style.css @@ -5,7 +5,7 @@ body { color: #111111; background-color: white; margin: 0; - min-width: 600px; + min-width: 700px; } #map { /* set size via JS */ @@ -18,22 +18,39 @@ body { /*padding-right: 15px; */ } -#info { +#info { margin-top: 10px; - border: lightgray groove thin; display: none; padding: 5px; - overflow: auto; } + #info a { padding-right: 5px; } +.route_results { + max-height: 40%; +} + +#input { + min-height: 400px; + max-width: 270px; +} + +.instructions_info { + overflow: auto; +} + +.route_description { + padding: 7px 5px; +} + #input, #instructions, #footer { width: 280px; } + .pointInput { - width: 235px; + width: 220px; float: left; } @@ -158,6 +175,7 @@ body { #searchButton { float: right; margin-bottom: 5px; + margin-right: 5px; } #searchButton:hover { @@ -202,16 +220,17 @@ tr.instruction { cursor: pointer; border-bottom: #dadada dashed thin; } -#instructions { + +.instructions { table-layout:fixed; border-collapse: collapse; padding-top: 10px; - width: 100%; + width: 98%; font-size: smaller; } -#instructions th, #instructions td { - padding: 6px 3px; +.instructions td { + padding: 7px 5px; } td.instr_title { @@ -224,7 +243,7 @@ td.instr_distance { text-align: right; } -#instructions tr .instr_pic { +.instructions tr .instr_pic { width: 20px; } td img.pic { @@ -300,8 +319,11 @@ td img.pic { padding-left: 2px; } .pointDelete:hover, .pointAdd:hover { cursor: pointer; } +.pointDelete:disabled { + background: red; +} -#expandDetails { +.expandDetails { color: gray; font-size:11px; float: right; @@ -309,7 +331,7 @@ td img.pic { width: 20px; height: 20px; height: 20px; - margin: 0px; + margin: 0px 10px; padding: 0px; background-image: linear-gradient(to bottom, white, #e7e7e7); } @@ -338,6 +360,40 @@ td img.pic { background: #00cc33; } +.alt_route_img { + padding-left: 2px; + margin-bottom: -3px; +} + +#route_result_tabs { + list-style: none; + padding: 0; + margin: 0; +} + +#route_result_tabs li { + background-color: #FFF; + cursor: pointer; + float: left; + padding: 5px 10px; +} + +#route_result_tabs li.current { + font-weight: bold; + background-image: linear-gradient(to bottom, #e7e7e7, white); + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.route_result_tab { + border: lightgray groove thin; + display: none; +} + +.route_result_tab.current { + display: block; +} + #donate_form { padding: 0; } diff --git a/web/src/main/webapp/img/alt_route.png b/web/src/main/webapp/img/alt_route.png new file mode 100644 index 0000000000000000000000000000000000000000..3f4aaf6fb9ea02fce562c88062a0df5b9321142d GIT binary patch literal 562 zcmV-20?qx2P)IKN z(j_l?2)igkiggjfi)Rllow{_Af*=qQ4|@}2MeDExyVJN}m%_mO`#SVHOf$1<$j1I) z4)6Eg_kG^H-*4WCs?v&;XbH7hU=f)Uku?!{CL*^*#5z;cj8==tI$#?!4g3VwfED0_ zs&0ohb>CP-Zi-0eAK9OP&#IaMdVr_E5%3(?6p?|5LRBLV&M&aCcXe-R@zArWM8O_~N4tY3W8dzb"); +module.exports.create = function (mapLayer, path, urlForHistory, request) { + var instructionsElement = $(""); var partialInstr = path.instructions.length > 100; var len = Math.min(path.instructions.length, 100); @@ -58,7 +58,8 @@ module.exports.addInstructions = function(mapLayer, path, urlForHistory, request var lngLat = path.points.coordinates[instr.interval[0]]; addInstruction(mapLayer, instructionsElement, instr, m, lngLat); } - $("#info").append(instructionsElement); + var infoDiv = $("
"); + infoDiv.append(instructionsElement); if (partialInstr) { var moreDiv = $(""); @@ -76,11 +77,11 @@ module.exports.addInstructions = function(mapLayer, path, urlForHistory, request var hiddenDiv = $("
"); hiddenDiv.hide(); - var toggly = $(""); + var toggly = $(""); toggly.click(function () { hiddenDiv.toggle(); }); - $("#info").append(toggly); + infoDiv.append(toggly); var infoStr = "points: " + path.points.coordinates.length; hiddenDiv.append("" + infoStr + ""); @@ -120,5 +121,6 @@ module.exports.addInstructions = function(mapLayer, path, urlForHistory, request if (metaVersionInfo) hiddenDiv.append(metaVersionInfo); - $("#info").append(hiddenDiv); + infoDiv.append(hiddenDiv); + return infoDiv; }; diff --git a/web/src/main/webapp/js/main-template.js b/web/src/main/webapp/js/main-template.js index 3556b0ba70c..1ba52a15e8c 100644 --- a/web/src/main/webapp/js/main-template.js +++ b/web/src/main/webapp/js/main-template.js @@ -292,9 +292,9 @@ function checkInput() { (toFrom === FROM) ? 'img/marker-small-green.png' : ((toFrom === TO) ? 'img/marker-small-red.png' : 'img/marker-small-blue.png')); if (len > 2) { - div.find(".pointDelete").click(deleteClickHandler).show(); + div.find(".pointDelete").click(deleteClickHandler).prop('disabled', false).removeClass('ui-state-disabled'); } else { - div.find(".pointDelete").hide(); + div.find(".pointDelete").prop('disabled', true).addClass('ui-state-disabled'); } autocomplete.showListForIndex(ghRequest, routeIfAllResolved, i); @@ -308,8 +308,6 @@ function checkInput() { $(input).attr("placeholder", translate.tr("viaHint")); } } - - mapLayer.adjustMapSize(); } function setToStart(e) { @@ -459,11 +457,11 @@ function routeLatLng(request, doQuery) { History.pushState(params, messages.browserTitle, urlForHistory); return; } - - $("#info").empty(); - $("#info").show(); - var descriptionDiv = $("
"); - $("#info").append(descriptionDiv); + var infoDiv = $("#info"); + infoDiv.empty(); + infoDiv.show(); + var routeResultsDiv = $("
"); + infoDiv.append(routeResultsDiv); mapLayer.clearElevation(); mapLayer.clearLayers(); @@ -475,59 +473,122 @@ function routeLatLng(request, doQuery) { $("button#" + request.getVehicle().toLowerCase()).addClass("selectvehicle"); var urlForAPI = request.createURL(); - descriptionDiv.html(' Search Route ...'); + routeResultsDiv.html(' Search Route ...'); request.doRequest(urlForAPI, function (json) { - descriptionDiv.html(""); + routeResultsDiv.html(""); if (json.message) { var tmpErrors = json.message; log(tmpErrors); if (json.hints) { for (var m = 0; m < json.hints.length; m++) { - descriptionDiv.append("
" + json.hints[m].message + "
"); + routeResultsDiv.append("
" + json.hints[m].message + "
"); } } else { - descriptionDiv.append("
" + tmpErrors + "
"); + routeResultsDiv.append("
" + tmpErrors + "
"); } return; } - var path = json.paths[0]; - var geojsonFeature = { - "type": "Feature", - // "style": myStyle, - "geometry": path.points - }; - if (request.hasElevation()) { - mapLayer.addElevation(geojsonFeature); + function createClickHandler(geoJsons, currentLayerIndex, tabHeader, oneTab, hasElevation) { + return function () { + + var currentGeoJson = geoJsons[currentLayerIndex]; + mapLayer.eachLayer(function (layer) { + // skip markers etc + if (!layer.setStyle) + return; + + var doHighlight = layer.feature === currentGeoJson; + layer.setStyle(doHighlight ? highlightRouteStyle : alternativeRouteStye); + if (doHighlight) { + if (!L.Browser.ie && !L.Browser.opera) + layer.bringToFront(); + } + }); + + if (hasElevation) { + mapLayer.clearElevation(); + mapLayer.addElevation(currentGeoJson); + } + + headerTabs.find("li").removeClass("current"); + routeResultsDiv.find("div").removeClass("current"); + + tabHeader.addClass("current"); + oneTab.addClass("current"); + }; } - mapLayer.addDataToRoutingLayer(geojsonFeature); - if (path.bbox && doZoom) { - var minLon = path.bbox[0]; - var minLat = path.bbox[1]; - var maxLon = path.bbox[2]; - var maxLat = path.bbox[3]; - var tmpB = new L.LatLngBounds(new L.LatLng(minLat, minLon), new L.LatLng(maxLat, maxLon)); - mapLayer.fitMapToBounds(tmpB); + var headerTabs = $("
    "); + if (json.paths.length > 1) { + routeResultsDiv.append(headerTabs); + routeResultsDiv.append("
    "); } - var tmpTime = translate.createTimeString(path.time); - var tmpDist = translate.createDistanceString(path.distance); - var tmpEleInfoStr = ""; - if (request.hasElevation()) - tmpEleInfoStr = translate.createEleInfoString(path.ascend, path.descend); + // the routing layer uses the geojson properties.style for the style, see map.js + var defaultRouteStyle = {color: "#00cc33", "weight": 5, "opacity": 0.6}; + var highlightRouteStyle = {color: "#00cc33", "weight": 6, "opacity": 0.8}; + var alternativeRouteStye = {color: "darkgray", "weight": 6, "opacity": 0.8}; + var geoJsons = []; + var firstHeader; + + for (var pathIndex = 0; pathIndex < json.paths.length; pathIndex++) { + var tabHeader = $("
  • ").append((pathIndex + 1) + ""); + if (pathIndex === 0) + firstHeader = tabHeader; + + headerTabs.append(tabHeader); + var path = json.paths[pathIndex]; + var style = (pathIndex === 0) ? defaultRouteStyle : alternativeRouteStye; + + var geojsonFeature = { + "type": "Feature", + "geometry": path.points, + "properties": {"style": style} + }; + + geoJsons.push(geojsonFeature); + mapLayer.addDataToRoutingLayer(geojsonFeature); + var oneTab = $("
    "); + routeResultsDiv.append(oneTab); + tabHeader.click(createClickHandler(geoJsons, pathIndex, tabHeader, oneTab, request.hasElevation())); + + var tmpTime = translate.createTimeString(path.time); + var tmpDist = translate.createDistanceString(path.distance); + var routeInfo = $("
    "); + if (path.description && path.description.length > 0) { + routeInfo.text(path.description); + routeInfo.append("
    "); + } + routeInfo.append(translate.tr("routeInfo", [tmpDist, tmpTime])); + if (request.hasElevation()) { + routeInfo.append(translate.createEleInfoString(path.ascend, path.descend)); + } + oneTab.append(routeInfo); - descriptionDiv.append(translate.tr("routeInfo", [tmpDist, tmpTime])); - descriptionDiv.append(tmpEleInfoStr); + if (path.instructions) { + var instructions = require('./instructions.js'); + oneTab.append(instructions.create(mapLayer, path, urlForHistory, request)); + } + } + // already select best path + firstHeader.click(); + + mapLayer.adjustMapSize(); + // TODO change bounding box on click + var firstPath = json.paths[0]; + if (firstPath.bbox && doZoom) { + var minLon = firstPath.bbox[0]; + var minLat = firstPath.bbox[1]; + var maxLon = firstPath.bbox[2]; + var maxLat = firstPath.bbox[3]; + var tmpB = new L.LatLngBounds(new L.LatLng(minLat, minLon), new L.LatLng(maxLat, maxLon)); + mapLayer.fitMapToBounds(tmpB); + } $('.defaulting').each(function (index, element) { $(element).css("color", "black"); }); - - if (path.instructions) { - var instructions = require('./instructions.js'); - instructions.addInstructions(mapLayer, path, urlForHistory, request); - } }); } diff --git a/web/src/main/webapp/js/main.js b/web/src/main/webapp/js/main.js index 0fd165ef9fb..db22f8dd4a8 100644 --- a/web/src/main/webapp/js/main.js +++ b/web/src/main/webapp/js/main.js @@ -23,7 +23,7 @@ case"touchend":return this.addPointerListenerEnd(t,e,i,n);case"touchmove":return var formatTools=require("./tools/format.js"),GHInput=require("./graphhopper/GHInput.js"),mapLayer=require("./map.js"),dataToHtml=function(t,e){var o="";t.name&&(o+="
    "+formatTools.formatValue(t.name,e)+"
    ");var a="";return t.postcode&&(a=t.postcode),t.city&&(a=formatTools.insComma(a,t.city)),t.country&&(a=formatTools.insComma(a,t.country)),a&&(o+="
    "+formatTools.formatValue(a,e)+"
    "),"highway"===t.osm_key,o+="place"===t.osm_key?""+t.osm_value+"":""+t.osm_key+""},dataToText=function(t){var e="";return t.name&&(e+=t.name),t.postcode&&(e=formatTools.insComma(e,t.postcode)),t.city&&e.indexOf(t.city)<0&&(e=formatTools.insComma(e,t.city)),t.country&&e.indexOf(t.country)<0&&(e=formatTools.insComma(e,t.country)),e},AutoComplete=function(t,e){this.host=t,this.key=e,this.dataType="json"};AutoComplete.prototype.createPath=function(t){for(var e in this.api_params){var o=this.api_params[e];if(GHRoute.isArray(o))for(var a in o)t+="&"+encodeURIComponent(e)+"="+encodeURIComponent(o[a]);else t+="&"+encodeURIComponent(e)+"="+encodeURIComponent(o)}return t},AutoComplete.prototype.createGeocodeURL=function(t,e){var o=this.createPath(this.host+"/geocode?limit=6&type="+this.dataType+"&key="+this.key);if(e>=0&&e div.pointDiv").eq(t).find(".pointInput")},AutoComplete.prototype.hide=function(){$(':input[id$="_Input"]').autocomplete().hide()},AutoComplete.prototype.showListForIndex=function(t,e,o){var a=this.getAutoCompleteDiv(o),n=this.createGeocodeURL(t,o-1),r={containerClass:"autocomplete",timeout:1e3,deferRequestBy:5,minChars:2,maxHeight:510,noCache:!0,triggerSelectOnValidInput:!1,autoSelectFirst:!1,paramName:"q",dataType:t.dataType,onSearchStart:function(t){var e=new GHInput(t.q).lat;return void 0===e},serviceUrl:function(){return n},transformResult:function(t,e){if(t.suggestions=[],t.hits)for(var o=0;oLyrk',subdomains:["a","b","c"]}),omniscale=L.tileLayer.wms("https://maps.omniscale.net/v1/mapsgraph-bf48cc0b/tile",{layers:"osm",attribution:osmAttr+', © Omniscale'}),mapquest=L.tileLayer("http://{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png",{attribution:osmAttr+', MapQuest',subdomains:["otile1","otile2","otile3","otile4"]}),mapquestAerial=L.tileLayer("http://{s}.mqcdn.com/tiles/1.0.0/sat/{z}/{x}/{y}.png",{attribution:osmAttr+', MapQuest',subdomains:["otile1","otile2","otile3","otile4"]}),openMapSurfer=L.tileLayer("http://korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}",{attribution:osmAttr+', GIScience Heidelberg'}),sorbianLang=L.tileLayer("http://map.dgpsonline.eu/osmsb/{z}/{x}/{y}.png",{attribution:osmAttr+', © Alberding GmbH, CC-BY-SA'}),thunderTransport=L.tileLayer("https://{s}.tile.thunderforest.com/transport/{z}/{x}/{y}.png",{attribution:osmAttr+', Thunderforest Transport',subdomains:["a","b","c"]}),thunderCycle=L.tileLayer("https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png",{attribution:osmAttr+', Thunderforest Cycle',subdomains:["a","b","c"]}),thunderOutdoors=L.tileLayer("https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png",{attribution:osmAttr+', Thunderforest Outdoors',subdomains:["a","b","c"]}),wrk=L.tileLayer("http://{s}.wanderreitkarte.de/topo/{z}/{x}/{y}.png",{attribution:osmAttr+', WanderReitKarte',subdomains:["topo4","topo","topo2","topo3"]}),osm=L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",{attribution:osmAttr}),osmde=L.tileLayer("http://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png",{attribution:osmAttr,subdomains:["a","b","c"]}),mapLink='Esri',wholink="i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community",esriAerial=L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",{attribution:"© "+mapLink+", "+wholink,maxZoom:18}),availableTileLayers={Lyrk:lyrk,Omniscale:omniscale,MapQuest:mapquest,"MapQuest Aerial":mapquestAerial,"Esri Aerial":esriAerial,OpenMapSurfer:openMapSurfer,"TF Transport":thunderTransport,"TF Cycle":thunderCycle,"TF Outdoors":thunderOutdoors,WanderReitKarte:wrk,OpenStreetMap:osm,"OpenStreetMap.de":osmde,"Sorbian Language":sorbianLang};module.exports.activeLayerName="Omniscale",module.exports.defaultLayer=omniscale,module.exports.getAvailableTileLayers=function(){return availableTileLayers},module.exports.selectLayer=function(t){var e=availableTileLayers[t];return e||(e=module.exports.defaultLayer),e}; +var osmAttr='© OpenStreetMap contributors',retinaTiles=L.Browser.retina,lyrk=L.tileLayer("https://tiles.lyrk.org/"+(retinaTiles?"lr":"ls")+"/{z}/{x}/{y}?apikey=6e8cfef737a140e2a58c8122aaa26077",{attribution:osmAttr+', Lyrk'}),omniscale=L.tileLayer.wms("https://maps.omniscale.net/v1/mapsgraph-bf48cc0b/tile",{layers:"osm",attribution:osmAttr+', © Omniscale'}),mapquest=L.tileLayer("http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png",{attribution:osmAttr+', MapQuest',subdomains:"1234"}),mapquestAerial=L.tileLayer("http://otile{s}.mqcdn.com/tiles/1.0.0/sat/{z}/{x}/{y}.png",{attribution:osmAttr+', MapQuest',subdomains:"1234"}),openMapSurfer=L.tileLayer("http://korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}",{attribution:osmAttr+', GIScience Heidelberg'}),sorbianLang=L.tileLayer("http://map.dgpsonline.eu/osmsb/{z}/{x}/{y}.png",{attribution:osmAttr+', © Alberding GmbH, CC-BY-SA'}),thunderTransport=L.tileLayer("https://{s}.tile.thunderforest.com/transport/{z}/{x}/{y}.png",{attribution:osmAttr+', Thunderforest Transport'}),thunderCycle=L.tileLayer("https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png",{attribution:osmAttr+', Thunderforest Cycle'}),thunderOutdoors=L.tileLayer("https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png",{attribution:osmAttr+', Thunderforest Outdoors'}),wrk=L.tileLayer("http://{s}.wanderreitkarte.de/topo/{z}/{x}/{y}.png",{attribution:osmAttr+', WanderReitKarte',subdomains:["topo4","topo","topo2","topo3"]}),osm=L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",{attribution:osmAttr}),osmde=L.tileLayer("http://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png",{attribution:osmAttr}),mapLink='Esri',wholink="i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community",esriAerial=L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",{attribution:"© "+mapLink+", "+wholink,maxZoom:18}),availableTileLayers={Lyrk:lyrk,Omniscale:omniscale,MapQuest:mapquest,"MapQuest Aerial":mapquestAerial,"Esri Aerial":esriAerial,OpenMapSurfer:openMapSurfer,"TF Transport":thunderTransport,"TF Cycle":thunderCycle,"TF Outdoors":thunderOutdoors,WanderReitKarte:wrk,OpenStreetMap:osm,"OpenStreetMap.de":osmde,"Sorbian Language":sorbianLang};module.exports.activeLayerName="Omniscale",module.exports.defaultLayer=omniscale,module.exports.getAvailableTileLayers=function(){return availableTileLayers},module.exports.selectLayer=function(t){var e=availableTileLayers[t];return e||(e=module.exports.defaultLayer),e}; },{}],7:[function(require,module,exports){ var ensureOneCheckboxSelected=function(){$("#gpx_route").change(function(){$(this).is(":checked")?($("#gpx_track").prop("disabled",!1),$("#gpx_waypoints").prop("disabled",!1)):$("#gpx_track").is(":checked")?$("#gpx_waypoints").is(":checked")||$("#gpx_track").prop("disabled",!0):$("#gpx_waypoints").prop("disabled",!0)}),$("#gpx_track").change(function(){$(this).is(":checked")?($("#gpx_route").prop("disabled",!1),$("#gpx_waypoints").prop("disabled",!1)):$("#gpx_route").is(":checked")?$("#gpx_waypoints").is(":checked")||$("#gpx_route").prop("disabled",!0):$("#gpx_waypoints").prop("disabled",!0)}),$("#gpx_waypoints").change(function(){$(this).is(":checked")?($("#gpx_route").prop("disabled",!1),$("#gpx_track").prop("disabled",!1)):$("#gpx_route").is(":checked")?$("#gpx_track").is(":checked")||$("#gpx_route").prop("disabled",!0):$("#gpx_track").prop("disabled",!0)})};module.exports.addGpxExport=function(e){function p(p,o,i){return e.route.isResolved()&&window.open(e.createGPXURL(p,o,i)),!1}function o(){return p($("#gpx_route").is(":checked"),$("#gpx_track").is(":checked"),$("#gpx_waypoints").is(":checked")),i.dialog("close"),!1}var i;$(function(){i=$("#gpx_dialog").dialog({width:420,height:260,autoOpen:!1,resizable:!1,draggable:!1,buttons:{"Export GPX":o,Cancel:function(){$(this).dialog("close")}}}),ensureOneCheckboxSelected()}),$("#gpxExportButton a").click(function(e){e.preventDefault(),$("#gpx_dialog").dialog("open")})}; @@ -41,7 +41,7 @@ var GHInput=require("./GHInput.js"),GHroute=function(){var t=Object.create(Array var decodePath=function(e,d){for(var o=e.length,r=0,a=[],h=0,t=0,c=0;o>r;){var l,v=0,i=0;do l=e.charCodeAt(r++)-63,i|=(31&l)<=32);var u=1&i?~(i>>1):i>>1;h+=u,v=0,i=0;do l=e.charCodeAt(r++)-63,i|=(31&l)<=32);var n=1&i?~(i>>1):i>>1;if(t+=n,d){v=0,i=0;do l=e.charCodeAt(r++)-63,i|=(31&l)<=32);var s=1&i?~(i>>1):i>>1;c+=s,a.push([1e-5*t,1e-5*h,c/100])}else a.push([1e-5*t,1e-5*h])}return a};module.exports.decodePath=decodePath; },{}],12:[function(require,module,exports){ -function addInstruction(t,e,n,a,r){var o=n.sign;o=0===a?"marker-icon-green":messages.getSignName(o);var i=n.text;n.annotation_text&&(i=i?i+", "+n.annotation_text:n.annotation_text);var s=$("
");if("continue"!==o){var p="";s.append("")}else s.append(""),r&&s.click(function(){routeSegmentPopup&&t.removeLayerFromMap(routeSegmentPopup),routeSegmentPopup=L.popup().setLatLng([r[1],r[0]]).setContent(i).openOn(t.getMap())}),e.append(s)}var translate=require("./translate.js"),messages=require("./messages.js"),routeSegmentPopup=null;module.exports.addInstructions=function(t,e,n,a){for(var r=$("
"+p+"");var d=$("");d.text(i),s.append(d);var c=n.distance;c>0&&s.append(""+translate.createDistanceString(c)+"
"+translate.createTimeString(n.time)+"
"),o=e.instructions.length>100,i=Math.min(e.instructions.length,100),s=0;i>s;s++){var p=e.instructions[s],d=e.points.coordinates[p.interval[0]];addInstruction(t,r,p,s,d)}if($("#info").append(r),o){var c=$("");c.click(function(){c.remove();for(var n=i;n");l.hide();var g=$("");g.click(function(){l.toggle()}),$("#info").append(g);var m="points: "+e.points.coordinates.length;l.append(""+m+"");var u=$("#export-link a");u.attr("href",n);var f=$("
view on OSM"),v="bicycle";"FOOT"===a.getVehicle().toUpperCase()&&(v="foot"),f.attr("href","http://www.openstreetmap.org/directions?engine=graphhopper_"+v+"&route="+encodeURIComponent(a.from.lat+","+a.from.lng+";"+a.to.lat+","+a.to.lng)),l.append(f);var h=$("OSRM");h.attr("href","http://map.project-osrm.org/?loc="+a.from+"&loc="+a.to),l.append("
Compare with: "),l.append(h);var b=$("Google "),w="",x="";"FOOT"===a.getVehicle().toUpperCase()?(w="&dirflg=w",x="&mode=W"):(a.getVehicle().toUpperCase().indexOf("BIKE")>=0||"MTB"===a.getVehicle().toUpperCase())&&(w="&dirflg=b"),b.attr("href","https://maps.google.com/?saddr="+a.from+"&daddr="+a.to+w),l.append(b);var S=$("Bing ");S.attr("href","https://www.bing.com/maps/default.aspx?rtp=adr."+a.from+"~adr."+a.to+x),l.append(S),metaVersionInfo&&l.append(metaVersionInfo),$("#info").append(l)}; +function addInstruction(t,e,n,a,r){var o=n.sign;o=0===a?"marker-icon-green":messages.getSignName(o);var i=n.text;n.annotation_text&&(i=i?i+", "+n.annotation_text:n.annotation_text);var s=$("
");if("continue"!==o){var p="";s.append("")}else s.append(""),r&&s.click(function(){routeSegmentPopup&&t.removeLayerFromMap(routeSegmentPopup),routeSegmentPopup=L.popup().setLatLng([r[1],r[0]]).setContent(i).openOn(t.getMap())}),e.append(s)}var translate=require("./translate.js"),messages=require("./messages.js"),routeSegmentPopup=null;module.exports.create=function(t,e,n,a){for(var r=$("
"+p+"");var c=$("");c.text(i),s.append(c);var d=n.distance;d>0&&s.append(""+translate.createDistanceString(d)+"
"+translate.createTimeString(n.time)+"
"),o=e.instructions.length>100,i=Math.min(e.instructions.length,100),s=0;i>s;s++){var p=e.instructions[s],c=e.points.coordinates[p.interval[0]];addInstruction(t,r,p,s,c)}var d=$("
");if(d.append(r),o){var l=$("");l.click(function(){l.remove();for(var n=i;n");g.hide();var u=$("");u.click(function(){g.toggle()}),d.append(u);var m="points: "+e.points.coordinates.length;g.append(""+m+"");var v=$("#export-link a");v.attr("href",n);var f=$("
view on OSM"),h="bicycle";"FOOT"===a.getVehicle().toUpperCase()&&(h="foot"),f.attr("href","http://www.openstreetmap.org/directions?engine=graphhopper_"+h+"&route="+encodeURIComponent(a.from.lat+","+a.from.lng+";"+a.to.lat+","+a.to.lng)),g.append(f);var b=$("OSRM");b.attr("href","http://map.project-osrm.org/?loc="+a.from+"&loc="+a.to),g.append("
Compare with: "),g.append(b);var w=$("Google "),x="",S="";"FOOT"===a.getVehicle().toUpperCase()?(x="&dirflg=w",S="&mode=W"):(a.getVehicle().toUpperCase().indexOf("BIKE")>=0||"MTB"===a.getVehicle().toUpperCase())&&(x="&dirflg=b"),w.attr("href","https://maps.google.com/?saddr="+a.from+"&daddr="+a.to+x),g.append(w);var _=$("Bing ");return _.attr("href","https://www.bing.com/maps/default.aspx?rtp=adr."+a.from+"~adr."+a.to+S),g.append(_),metaVersionInfo&&g.append(metaVersionInfo),d.append(g),d}; },{"./messages.js":21,"./translate.js":28}],13:[function(require,module,exports){ L.Control.Elevation=L.Control.extend({options:{position:"topright",theme:"lime-theme",width:600,height:175,margins:{top:10,right:20,bottom:30,left:60},useHeightIndicator:!0,interpolation:"linear",hoverNumber:{decimalsX:3,decimalsY:0,formatter:void 0},xTicks:void 0,yTicks:void 0,collapsed:!1,yAxisMin:void 0,yAxisMax:void 0,forceAxisBounds:!1},onRemove:function(){this._container=null},onAdd:function(t){this._map=t;var i=this.options,e=i.margins;i.xTicks=i.xTicks||Math.round(this._width()/75),i.yTicks=i.yTicks||Math.round(this._height()/30),i.hoverNumber.formatter=i.hoverNumber.formatter||this._formatter,d3.select("body").classed(i.theme,!0);var a=this._x=d3.scale.linear().range([0,this._width()]),s=this._y=d3.scale.linear().range([this._height(),0]),n=(this._area=d3.svg.area().interpolate(i.interpolation).x(function(t){return a(t.dist)}).y0(this._height()).y1(function(t){return s(t.altitude)}),this._container=L.DomUtil.create("div","elevation"));this._initToggle();var r=d3.select(n);r.attr("width",i.width);var o=r.append("svg");o.attr("width",i.width).attr("class","background").attr("height",i.height).append("g").attr("transform","translate("+e.left+","+e.top+")");var h=d3.svg.line();h=h.x(function(){return d3.mouse(o.select("g"))[0]}).y(function(){return this._height()});var d=d3.select(this._container).select("svg").select("g");this._areapath=d.append("path").attr("class","area");var l=this._background=d.append("rect").attr("width",this._width()).attr("height",this._height()).style("fill","none").style("stroke","none").style("pointer-events","all");L.Browser.touch?(l.on("touchmove.drag",this._dragHandler.bind(this)).on("touchstart.drag",this._dragStartHandler.bind(this)).on("touchstart.focus",this._mousemoveHandler.bind(this)),L.DomEvent.on(this._container,"touchend",this._dragEndHandler,this)):(l.on("mousemove.focus",this._mousemoveHandler.bind(this)).on("mouseout.focus",this._mouseoutHandler.bind(this)).on("mousedown.drag",this._dragStartHandler.bind(this)).on("mousemove.drag",this._dragHandler.bind(this)),L.DomEvent.on(this._container,"mouseup",this._dragEndHandler,this)),this._xaxisgraphicnode=d.append("g"),this._yaxisgraphicnode=d.append("g"),this._appendXaxis(this._xaxisgraphicnode),this._appendYaxis(this._yaxisgraphicnode);var c=this._focusG=d.append("g");return this._mousefocus=c.append("svg:line").attr("class","mouse-focus-line").attr("x2","0").attr("y2","0").attr("x1","0").attr("y1","0"),this._focuslabelX=c.append("svg:text").style("pointer-events","none").attr("class","mouse-focus-label-x"),this._focuslabelY=c.append("svg:text").style("pointer-events","none").attr("class","mouse-focus-label-y"),this._data&&this._applyData(),n},_dragHandler:function(){d3.event.preventDefault(),d3.event.stopPropagation(),this._gotDragged=!0,this._drawDragRectangle()},_drawDragRectangle:function(){if(this._dragStartCoords){var t=this._dragCurrentCoords=d3.mouse(this._background.node()),i=Math.min(this._dragStartCoords[0],t[0]),e=Math.max(this._dragStartCoords[0],t[0]);if(this._dragRectangle||this._dragRectangleG)this._dragRectangle.attr("width",e-i).attr("x",i);else{var a=d3.select(this._container).select("svg").select("g");this._dragRectangleG=a.append("g"),this._dragRectangle=this._dragRectangleG.append("rect").attr("width",e-i).attr("height",this._height()).attr("x",i).attr("class","mouse-drag").style("pointer-events","none")}}},_resetDrag:function(){this._dragRectangleG&&(this._dragRectangleG.remove(),this._dragRectangleG=null,this._dragRectangle=null,this._hidePositionMarker(),this._map.fitBounds(this._fullExtent))},_dragEndHandler:function(){if(!this._dragStartCoords||!this._gotDragged)return this._dragStartCoords=null,this._gotDragged=!1,void this._resetDrag();this._hidePositionMarker();var t=this._findItemForX(this._dragStartCoords[0]),i=this._findItemForX(this._dragCurrentCoords[0]);this._fitSection(t,i),this._dragStartCoords=null,this._gotDragged=!1},_dragStartHandler:function(){d3.event.preventDefault(),d3.event.stopPropagation(),this._gotDragged=!1,this._dragStartCoords=d3.mouse(this._background.node())},_findItemForX:function(t){var i=d3.bisector(function(t){return t.dist}).left,e=this._x.invert(t);return i(this._data,e)},_fitSection:function(t,i){var e=Math.min(t,i),a=Math.max(t,i),s=this._calculateFullExtent(this._data.slice(e,a));this._map.fitBounds(s)},_initToggle:function(){var t=this._container;if(t.setAttribute("aria-haspopup",!0),L.Browser.touch?L.DomEvent.on(t,"click",L.DomEvent.stopPropagation):L.DomEvent.disableClickPropagation(t),this.options.collapsed){this._collapse(),L.Browser.android||L.DomEvent.on(t,"mouseover",this._expand,this).on(t,"mouseout",this._collapse,this);var i=this._button=L.DomUtil.create("a","elevation-toggle",t);i.href="#",i.title="Elevation",L.Browser.touch?L.DomEvent.on(i,"click",L.DomEvent.stop).on(i,"click",this._expand,this):L.DomEvent.on(i,"focus",this._expand,this),this._map.on("click",this._collapse,this)}},_expand:function(){this._container.className=this._container.className.replace(" elevation-collapsed","")},_collapse:function(){L.DomUtil.addClass(this._container,"elevation-collapsed")},_width:function(){var t=this.options;return t.width-t.margins.left-t.margins.right},_height:function(){var t=this.options;return t.height-t.margins.top-t.margins.bottom},_formatter:function(t,i,e){var a;a=0===i?Math.round(t)+"":L.Util.formatNum(t,i)+"";var s=a.split(".");if(s[1]){for(var n=i-s[1].length;n>0;n--)s[1]+="0";a=s.join(e||".")}return a},_appendYaxis:function(t){t.attr("class","y axis").call(d3.svg.axis().scale(this._y).ticks(this.options.yTicks).orient("left")).append("text").attr("x",-36).attr("y",3).style("text-anchor","end").text("m")},_appendXaxis:function(t){t.attr("class","x axis").attr("transform","translate(0,"+this._height()+")").call(d3.svg.axis().scale(this._x).ticks(this.options.xTicks).orient("bottom")).append("text").attr("x",this._width()+20).attr("y",15).style("text-anchor","end").text("km")},_updateAxis:function(){this._xaxisgraphicnode.selectAll("g").remove(),this._xaxisgraphicnode.selectAll("path").remove(),this._xaxisgraphicnode.selectAll("text").remove(),this._yaxisgraphicnode.selectAll("g").remove(),this._yaxisgraphicnode.selectAll("path").remove(),this._yaxisgraphicnode.selectAll("text").remove(),this._appendXaxis(this._xaxisgraphicnode),this._appendYaxis(this._yaxisgraphicnode)},_mouseoutHandler:function(){this._hidePositionMarker()},_hidePositionMarker:function(){this._marker&&(this._map.removeLayer(this._marker),this._marker=null),this._mouseHeightFocus&&(this._mouseHeightFocus.style("visibility","hidden"),this._mouseHeightFocusLabel.style("visibility","hidden")),this._pointG&&this._pointG.style("visibility","hidden"),this._focusG.style("visibility","hidden")},_mousemoveHandler:function(){if(this._data&&0!==this._data.length){var t=d3.mouse(this._background.node()),i=this.options;this._focusG.style("visibility","visible"),this._mousefocus.attr("x1",t[0]).attr("y1",0).attr("x2",t[0]).attr("y2",this._height()).classed("hidden",!1);var e=(d3.bisector(function(t){return t.dist}).left,this._data[this._findItemForX(t[0])]),a=e.altitude,s=e.dist,n=e.latlng,r=i.hoverNumber.formatter(a,i.hoverNumber.decimalsY),o=i.hoverNumber.formatter(s,i.hoverNumber.decimalsX);this._focuslabelX.attr("x",t[0]).text(r+" m"),this._focuslabelY.attr("y",this._height()-5).attr("x",t[0]).text(o+" km");var h=this._map.latLngToLayerPoint(n);if(i.useHeightIndicator){if(!this._mouseHeightFocus){var d=d3.select(".leaflet-overlay-pane svg").append("g");this._mouseHeightFocus=d.append("svg:line").attr("class","height-focus line").attr("x2","0").attr("y2","0").attr("x1","0").attr("y1","0");var l=this._pointG=d.append("g");l.append("svg:circle").attr("r",6).attr("cx",0).attr("cy",0).attr("class","height-focus circle-lower"),this._mouseHeightFocusLabel=d.append("svg:text").attr("class","height-focus-label").style("pointer-events","none")}var c=this._height()/this._maxElevation*a,_=h.y-c;this._mouseHeightFocus.attr("x1",h.x).attr("x2",h.x).attr("y1",h.y).attr("y2",_).style("visibility","visible"),this._pointG.attr("transform","translate("+h.x+","+h.y+")").style("visibility","visible"),this._mouseHeightFocusLabel.attr("x",h.x).attr("y",_).text(a+" m").style("visibility","visible")}else this._marker?this._marker.setLatLng(n):this._marker=new L.Marker(n).addTo(this._map)}},_addGeoJSONData:function(t){if(t){for(var i=this._data||[],e=this._dist||0,a=this._maxElevation||0,s=0;si[1]||e.forceAxisBounds)&&(i[1]=e.yAxisMax),this._x.domain(t),this._y.domain(i),this._areapath.datum(this._data).attr("d",this._area),this._updateAxis(),this._fullExtent=this._calculateFullExtent(this._data)},_clearData:function(){this._data=null,this._dist=null,this._maxElevation=null},clear:function(){this._clearData(),this._areapath&&(this._areapath.attr("d","M0 0"),this._x.domain([0,1]),this._y.domain([0,1]),this._updateAxis())}}),L.control.elevation=function(t){return new L.Control.Elevation(t)}; @@ -60,7 +60,7 @@ s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof !function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):e(jQuery)}(function(e){"use strict";function t(n,s){var i=function(){},o=this,l={autoSelectFirst:!1,appendTo:"body",serviceUrl:null,lookup:null,onSelect:null,width:"auto",minChars:1,maxHeight:300,deferRequestBy:0,params:{},formatResult:t.formatResult,onPreSelect:i,delimiter:null,zIndex:9999,type:"GET",noCache:!1,onSearchStart:i,onSearchComplete:i,onSearchError:i,containerClass:"autocomplete-suggestions",tabDisabled:!1,dataType:"text",currentRequest:null,triggerSelectOnValidInput:!0,lookupFilter:function(e,t,n){return-1!==e.value.toLowerCase().indexOf(n)},paramName:"query",transformResult:function(t){return"string"==typeof t?e.parseJSON(t):t}};o.element=n,o.el=e(n),o.suggestions=[],o.badQueries=[],o.selectedIndex=-1,o.currentValue=o.element.value,o.intervalId=0,o.cachedResponse={},o.onChangeInterval=null,o.onChange=null,o.isLocal=!1,o.suggestionsContainer=null,o.options=e.extend({},l,s),o.classes={selected:"autocomplete-selected",suggestion:"autocomplete-suggestion"},o.hint=null,o.hintValue="",o.selection=null,o.initialize(),o.setOptions(s)}var n=function(){return{escapeRegExChars:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},createNode:function(e){var t=document.createElement("div");return t.className=e,t.style.position="absolute",t.style.display="none",t.style.cursor="pointer",t}}}(),s={ESC:27,TAB:9,RETURN:13,LEFT:37,UP:38,RIGHT:39,DOWN:40};t.utils=n,e.Autocomplete=t,t.formatResult=function(e,t){var s="("+n.escapeRegExChars(t)+")";return e.value.replace(new RegExp(s,"gi"),"$1")},t.prototype={killerFn:null,initialize:function(){var n,s=this,i="."+s.classes.suggestion,o=(s.classes.selected,s.options);s.element.setAttribute("autocomplete","off"),s.killerFn=function(t){0===e(t.target).closest("."+s.options.containerClass).length&&(s.killSuggestions(),s.disableKillerFn())},s.suggestionsContainer=t.utils.createNode(o.containerClass),n=e(s.suggestionsContainer),n.appendTo(o.appendTo),"auto"!==o.width&&n.width(o.width),n.on("click.autocomplete",i,function(){s.select(e(this).data("index"))}),s.fixPosition(),s.fixPositionCapture=function(){s.visible&&s.fixPosition()},e(window).on("resize.autocomplete",s.fixPositionCapture),s.el.on("keydown.autocomplete",function(e){s.onKeyPress(e)}),s.el.on("keyup.autocomplete",function(e){s.onKeyUp(e)}),s.el.on("blur.autocomplete",function(){s.onBlur()}),s.el.on("focus.autocomplete",function(){s.onFocus()}),s.el.on("change.autocomplete",function(e){s.onKeyUp(e)})},onFocus:function(){var e=this;e.fixPosition(),e.options.minChars<=e.el.val().length},onBlur:function(){this.enableKillerFn()},setOptions:function(t){var n=this,s=n.options;e.extend(s,t),n.isLocal=e.isArray(s.lookup),n.isLocal&&(s.lookup=n.verifySuggestionsFormat(s.lookup)),e(n.suggestionsContainer).css({"max-height":s.maxHeight+"px",width:s.width+"px","z-index":s.zIndex})},clearCache:function(){this.cachedResponse={},this.badQueries=[]},clear:function(){this.clearCache(),this.currentValue="",this.suggestions=[]},disable:function(){var e=this;e.disabled=!0,e.currentRequest&&e.currentRequest.abort()},enable:function(){this.disabled=!1},fixPosition:function(){var t,n,s=this;"body"===s.options.appendTo&&(t=s.el.offset(),n={top:t.top+s.el.outerHeight()+"px",left:t.left+"px"},"auto"===s.options.width&&(n.width=s.el.outerWidth()-2+"px"),e(s.suggestionsContainer).css(n))},enableKillerFn:function(){var t=this;e(document).on("click.autocomplete",t.killerFn)},disableKillerFn:function(){var t=this;e(document).off("click.autocomplete",t.killerFn)},killSuggestions:function(){var e=this;e.stopKillSuggestions(),e.intervalId=window.setInterval(function(){e.hide(),e.stopKillSuggestions()},50)},stopKillSuggestions:function(){window.clearInterval(this.intervalId)},isCursorAtEnd:function(){var e,t=this,n=t.el.val().length,s=t.element.selectionStart;return"number"==typeof s?s===n:document.selection?(e=document.selection.createRange(),e.moveStart("character",-n),n===e.text.length):!0},onKeyPress:function(e){var t=this;if(!t.disabled&&!t.visible&&e.which===s.DOWN&&t.currentValue)return void t.suggest();if(!t.disabled&&t.visible){switch(e.which){case s.ESC:t.el.val(t.currentValue),t.hide();break;case s.RIGHT:if(t.hint&&t.options.onHint&&t.isCursorAtEnd()){t.selectHint();break}return;case s.TAB:if(t.hint&&t.options.onHint)return void t.selectHint();case s.RETURN:if(-1===t.selectedIndex)return void t.hide();if(t.select(t.selectedIndex),e.which===s.TAB&&t.options.tabDisabled===!1)return;break;case s.UP:t.moveUp();break;case s.DOWN:t.moveDown();break;default:return}e.stopImmediatePropagation(),e.preventDefault()}},onKeyUp:function(e){var t=this;if(!t.disabled){switch(e.which){case s.UP:case s.DOWN:return}clearInterval(t.onChangeInterval),t.currentValue!==t.el.val()&&(t.findBestHint(),t.options.deferRequestBy>0?t.onChangeInterval=setInterval(function(){t.onValueChange()},t.options.deferRequestBy):t.onValueChange())}},onValueChange:function(){var t,n=this,s=n.options,i=n.el.val(),o=n.getQuery(i);return n.selection&&(n.selection=null,(s.onInvalidateSelection||e.noop).call(n.element)),clearInterval(n.onChangeInterval),n.currentValue=i,n.selectedIndex=-1,s.triggerSelectOnValidInput&&(t=n.findSuggestionIndex(o),-1!==t)?void n.select(t):void(o.lengtha&&(n.suggestions=n.suggestions.slice(0,a)),n},getSuggestions:function(t){var n,s,i,o=this,l=o.options,a=l.serviceUrl;if(l.params[l.paramName]=t,s=l.ignoreParams?null:l.params,o.isLocal?n=o.getSuggestionsLocal(t):(e.isFunction(a)&&(a=a.call(o.element,t)),i=a+"?"+e.param(s||{}),n=o.cachedResponse[i]),n&&e.isArray(n.suggestions))o.suggestions=n.suggestions,o.suggest();else if(!o.isBadQuery(t)){if(l.onSearchStart.call(o.element,l.params)===!1)return;o.currentRequest&&o.currentRequest.abort(),o.currentRequest=e.ajax({url:a,data:s,type:l.type,dataType:l.dataType}).done(function(e){o.currentRequest=null,o.processResponse(e,t,i),l.onSearchComplete.call(o.element,t)}).fail(function(e,n,s){l.onSearchError.call(o.element,t,e,n,s)})}},isBadQuery:function(e){for(var t=this.badQueries,n=t.length;n--;)if(0===e.indexOf(t[n]))return!0;return!1},hide:function(){var t=this;t.visible=!1,t.selectedIndex=-1,e(t.suggestionsContainer).hide(),t.signalHint(null)},suggest:function(){if(0===this.suggestions.length)return void this.hide();var t,n,s=this,i=s.options,o=i.formatResult,l=s.getQuery(s.currentValue),a=s.classes.suggestion,r=s.classes.selected,u=e(s.suggestionsContainer),c=i.beforeRender,g="";return i.triggerSelectOnValidInput&&(t=s.findSuggestionIndex(l),-1!==t)?void s.select(t):(e.each(s.suggestions,function(e,t){g+='
'+o(t,l)+"
"}),"auto"===i.width&&(n=s.el.outerWidth()-2,u.width(n>0?n:300)),u.html(g),i.autoSelectFirst&&(s.selectedIndex=0,u.children().first().addClass(r)),e.isFunction(c)&&c.call(s.element,u),u.show(),s.visible=!0,void s.findBestHint())},findBestHint:function(){var t=this,n=t.el.val().toLowerCase(),s=null;n&&(e.each(t.suggestions,function(e,t){var i=0===t.value.toLowerCase().indexOf(n);return i&&(s=t),!i}),t.signalHint(s))},signalHint:function(t){var n="",s=this;t&&(n=s.currentValue+t.value.substr(s.currentValue.length)),s.hintValue!==n&&(s.hintValue=n,s.hint=t,(this.options.onHint||e.noop)(n))},verifySuggestionsFormat:function(t){return t.length&&"string"==typeof t[0]?e.map(t,function(e){return{value:e,data:null}}):t},processResponse:function(e,t,n){var s=this,i=s.options,o=i.transformResult(e,t);o.suggestions=s.verifySuggestionsFormat(o.suggestions),i.noCache||(s.cachedResponse[n]=o,0===o.suggestions.length&&s.badQueries.push(n)),t===s.getQuery(s.currentValue)&&(s.suggestions=o.suggestions,s.suggest())},activate:function(t){var n,s=this,i=s.classes.selected,o=e(s.suggestionsContainer),l=o.children();return s.selectedIndex===t?null:(o.children("."+i).removeClass(i),s.selectedIndex=t,-1!==s.selectedIndex&&l.length>s.selectedIndex?(n=l.get(s.selectedIndex),e(n).addClass(i),s.options.onPreSelect(s.suggestions[t],n),n):null)},selectHint:function(){var t=this,n=e.inArray(t.hint,t.suggestions);t.select(n)},select:function(e){var t=this;t.hide(),t.onSelect(e)},moveUp:function(){var t=this;if(-1!==t.selectedIndex)return 0===t.selectedIndex?(e(t.suggestionsContainer).children().first().removeClass(t.classes.selected),t.selectedIndex=-1,t.el.val(t.currentValue),void t.findBestHint()):void t.adjustScroll(t.selectedIndex-1)},moveDown:function(){var e=this;e.selectedIndex!==e.suggestions.length-1&&e.adjustScroll(e.selectedIndex+1)},adjustScroll:function(t){var n,s,i,o=this,l=o.activate(t),a=25;l&&(n=l.offsetTop,s=e(o.suggestionsContainer).scrollTop(),i=s+o.options.maxHeight-a,s>n?e(o.suggestionsContainer).scrollTop(n):n>i&&e(o.suggestionsContainer).scrollTop(n-o.options.maxHeight+a),o.el.val(o.getValue(o.suggestions[t].value)),o.signalHint(null))},onSelect:function(t){var n=this,s=n.options.onSelect,i=n.suggestions[t];n.currentValue=n.getValue(i.value),n.el.val(n.currentValue),n.signalHint(null),n.suggestions=[],n.selection=i,e.isFunction(s)&&s.call(n.element,i)},getValue:function(e){var t,n,s=this,i=s.options.delimiter;return i?(t=s.currentValue,n=t.split(i),1===n.length?e:t.substr(0,t.length-n[n.length-1].length)+e):e},dispose:function(){var t=this;t.el.off(".autocomplete").removeData("autocomplete"),t.disableKillerFn(),e(window).off("resize.autocomplete",t.fixPositionCapture),e(t.suggestionsContainer).remove()}},e.fn.autocomplete=function(n,s){var i="autocomplete";return 0===arguments.length?this.first().data(i):this.each(function(){var o=e(this),l=o.data(i);"string"==typeof n?l&&"function"==typeof l[n]&&l[n](s):(l&&l.dispose&&l.dispose(),l=new t(this,n),o.data(i,l))})}}); },{}],16:[function(require,module,exports){ -!function(e,t){"use strict";var r=e.History=e.History||{},a=e.jQuery;if("undefined"!=typeof r.Adapter)throw new Error("History.js Adapter has already been loaded...");r.Adapter={bind:function(e,t,r){a(e).bind(t,r)},trigger:function(e,t,r){a(e).trigger(t,r)},extractEventData:function(e,r,a){var n=r&&r.originalEvent&&r.originalEvent[e]||a&&a[e]||t;return n},onDomLoad:function(e){a(e)}},"undefined"!=typeof r.init&&r.init()}(window),function(e,t){"use strict";var r=e.console||t,a=e.document,n=e.navigator,o=!1,i=e.setTimeout,s=e.clearTimeout,u=e.setInterval,l=e.clearInterval,d=e.JSON,c=e.alert,p=e.History=e.History||{},f=e.history;try{o=e.sessionStorage,o.setItem("TEST","1"),o.removeItem("TEST")}catch(g){o=!1}if(d.stringify=d.stringify||d.encode,d.parse=d.parse||d.decode,"undefined"!=typeof p.init)throw new Error("History.js Core has already been loaded...");p.init=function(e){return"undefined"==typeof p.Adapter?!1:("undefined"!=typeof p.initCore&&p.initCore(),"undefined"!=typeof p.initHtml4&&p.initHtml4(),!0)},p.initCore=function(g){if("undefined"!=typeof p.initCore.initialized)return!1;if(p.initCore.initialized=!0,p.options=p.options||{},p.options.hashChangeInterval=p.options.hashChangeInterval||100,p.options.safariPollInterval=p.options.safariPollInterval||500,p.options.doubleCheckInterval=p.options.doubleCheckInterval||500,p.options.disableSuid=p.options.disableSuid||!1,p.options.storeInterval=p.options.storeInterval||1e3,p.options.busyDelay=p.options.busyDelay||250,p.options.debug=p.options.debug||!1,p.options.initialTitle=p.options.initialTitle||a.title,p.options.html4Mode=p.options.html4Mode||!1,p.options.delayInit=p.options.delayInit||!1,p.intervalList=[],p.clearAllIntervals=function(){var e,t=p.intervalList;if("undefined"!=typeof t&&null!==t){for(e=0;et;++t){if(i=arguments[t],"object"==typeof i&&"undefined"!=typeof d)try{i=d.stringify(i)}catch(l){}e+="\n"+i+"\n"}return u?(u.value+=e+"\n-----\n",u.scrollTop=u.scrollHeight-u.clientHeight):s||c(e),!0},p.getInternetExplorerMajorVersion=function(){var e=p.getInternetExplorerMajorVersion.cached="undefined"!=typeof p.getInternetExplorerMajorVersion.cached?p.getInternetExplorerMajorVersion.cached:function(){for(var e=3,t=a.createElement("div"),r=t.getElementsByTagName("i");(t.innerHTML="")&&r[0];);return e>4?e:!1}();return e},p.isInternetExplorer=function(){var e=p.isInternetExplorer.cached="undefined"!=typeof p.isInternetExplorer.cached?p.isInternetExplorer.cached:Boolean(p.getInternetExplorerMajorVersion());return e},p.options.html4Mode?p.emulated={pushState:!0,hashChange:!0}:p.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(n.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(n.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in a)||p.isInternetExplorer()&&p.getInternetExplorerMajorVersion()<8)},p.enabled=!p.emulated.pushState,p.bugs={setHash:Boolean(!p.emulated.pushState&&"Apple Computer, Inc."===n.vendor&&/AppleWebKit\/5([0-2]|3[0-3])/.test(n.userAgent)),safariPoll:Boolean(!p.emulated.pushState&&"Apple Computer, Inc."===n.vendor&&/AppleWebKit\/5([0-2]|3[0-3])/.test(n.userAgent)),ieDoubleCheck:Boolean(p.isInternetExplorer()&&p.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(p.isInternetExplorer()&&p.getInternetExplorerMajorVersion()<7)},p.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},p.cloneObject=function(e){var t,r;return e?(t=d.stringify(e),r=d.parse(t)):r={},r},p.getRootUrl=function(){var e=a.location.protocol+"//"+(a.location.hostname||a.location.host);return a.location.port&&(e+=":"+a.location.port),e+="/"},p.getBaseHref=function(){var e=a.getElementsByTagName("base"),t=null,r="";return 1===e.length&&(t=e[0],r=t.href.replace(/[^\/]+$/,"")),r=r.replace(/\/+$/,""),r&&(r+="/"),r},p.getBaseUrl=function(){var e=p.getBaseHref()||p.getBasePageUrl()||p.getRootUrl();return e},p.getPageUrl=function(){var e,t=p.getState(!1,!1),r=(t||{}).url||p.getLocationHref();return e=r.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,r){return/\./.test(e)?e:e+"/"})},p.getBasePageUrl=function(){var e=p.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,r){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},p.getFullUrl=function(e,t){var r=e,a=e.substring(0,1);return t="undefined"==typeof t?!0:t,/[a-z]+\:\/\//.test(e)||(r="/"===a?p.getRootUrl()+e.replace(/^\/+/,""):"#"===a?p.getPageUrl().replace(/#.*/,"")+e:"?"===a?p.getPageUrl().replace(/[\?#].*/,"")+e:t?p.getBaseUrl()+e.replace(/^(\.\/)+/,""):p.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),r.replace(/\#$/,"")},p.getShortUrl=function(e){var t=e,r=p.getBaseUrl(),a=p.getRootUrl();return p.emulated.pushState&&(t=t.replace(r,"")),t=t.replace(a,"/"),p.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,"")},p.getLocationHref=function(e){return e=e||a,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:-1==e.URL.indexOf("#")&&-1!=e.location.href.indexOf("#")?e.location.href:e.URL||e.location.href},p.store={},p.idToState=p.idToState||{},p.stateToId=p.stateToId||{},p.urlToId=p.urlToId||{},p.storedStates=p.storedStates||[],p.savedStates=p.savedStates||[],p.normalizeStore=function(){p.store.idToState=p.store.idToState||{},p.store.urlToId=p.store.urlToId||{},p.store.stateToId=p.store.stateToId||{}},p.getState=function(e,t){"undefined"==typeof e&&(e=!0),"undefined"==typeof t&&(t=!0);var r=p.getLastSavedState();return!r&&t&&(r=p.createStateObject()),e&&(r=p.cloneObject(r),r.url=r.cleanUrl||r.url),r},p.getIdByState=function(e){var t,r=p.extractId(e.url);if(!r)if(t=p.getStateString(e),"undefined"!=typeof p.stateToId[t])r=p.stateToId[t];else if("undefined"!=typeof p.store.stateToId[t])r=p.store.stateToId[t];else{for(;r=(new Date).getTime()+String(Math.random()).replace(/\D/g,""),"undefined"!=typeof p.idToState[r]||"undefined"!=typeof p.store.idToState[r];);p.stateToId[t]=r,p.idToState[r]=e}return r},p.normalizeState=function(e){var t,r;return e&&"object"==typeof e||(e={}),"undefined"!=typeof e.normalized?e:(e.data&&"object"==typeof e.data||(e.data={}),t={},t.normalized=!0,t.title=e.title||"",t.url=p.getFullUrl(e.url?e.url:p.getLocationHref()),t.hash=p.getShortUrl(t.url),t.data=p.cloneObject(e.data),t.id=p.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,r=!p.isEmptyObject(t.data),(t.title||r)&&p.options.disableSuid!==!0&&(t.hash=p.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=p.getFullUrl(t.hash),(p.emulated.pushState||p.bugs.safariPoll)&&p.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t)},p.createStateObject=function(e,t,r){var a={data:e,title:t,url:r};return a=p.normalizeState(a)},p.getStateById=function(e){e=String(e);var r=p.idToState[e]||p.store.idToState[e]||t;return r},p.getStateString=function(e){var t,r,a;return t=p.normalizeState(e),r={data:t.data,title:e.title,url:e.url},a=d.stringify(r)},p.getStateId=function(e){var t,r;return t=p.normalizeState(e),r=t.id},p.getHashByState=function(e){var t,r;return t=p.normalizeState(e),r=t.hash},p.extractId=function(e){var t,r,a,n;return n=-1!=e.indexOf("#")?e.split("#")[0]:e,r=/(.*)\&_suid=([0-9]+)$/.exec(n),a=r?r[1]||e:e,t=r?String(r[2]||""):"",t||!1},p.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},p.extractState=function(e,t){var r,a,n=null;return t=t||!1,r=p.extractId(e),r&&(n=p.getStateById(r)),n||(a=p.getFullUrl(e),r=p.getIdByUrl(a)||!1,r&&(n=p.getStateById(r)),!n&&t&&!p.isTraditionalAnchor(e)&&(n=p.createStateObject(null,null,a))),n},p.getIdByUrl=function(e){var r=p.urlToId[e]||p.store.urlToId[e]||t;return r},p.getLastSavedState=function(){return p.savedStates[p.savedStates.length-1]||t},p.getLastStoredState=function(){return p.storedStates[p.storedStates.length-1]||t},p.hasUrlDuplicate=function(e){var t,r=!1;return t=p.extractState(e.url),r=t&&t.id!==e.id},p.storeState=function(e){return p.urlToId[e.url]=e.id,p.storedStates.push(p.cloneObject(e)),e},p.isLastSavedState=function(e){var t,r,a,n=!1;return p.savedStates.length&&(t=e.id,r=p.getLastSavedState(),a=r.id,n=t===a),n},p.saveState=function(e){return p.isLastSavedState(e)?!1:(p.savedStates.push(p.cloneObject(e)),!0)},p.getStateByIndex=function(e){var t=null;return t="undefined"==typeof e?p.savedStates[p.savedStates.length-1]:0>e?p.savedStates[p.savedStates.length+e]:p.savedStates[e]},p.getCurrentIndex=function(){var e=null;return e=p.savedStates.length<1?0:p.savedStates.length-1},p.getHash=function(e){var t,r=p.getLocationHref(e);return t=p.getHashByUrl(r)},p.unescapeHash=function(e){var t=p.normalizeHash(e);return t=decodeURIComponent(t)},p.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},p.setHash=function(e,t){var r,n;return t!==!1&&p.busy()?(p.pushQueue({scope:p,callback:p.setHash,args:arguments,queue:t}),!1):(p.busy(!0),r=p.extractState(e,!0),r&&!p.emulated.pushState?p.pushState(r.data,r.title,r.url,!1):p.getHash()!==e&&(p.bugs.setHash?(n=p.getPageUrl(),p.pushState(null,null,n+"#"+e,!1)):a.location.hash=e),p)},p.escapeHash=function(t){var r=p.normalizeHash(t);return r=e.encodeURIComponent(r),p.bugs.hashEscape||(r=r.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),r},p.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=p.unescapeHash(t)},p.setTitle=function(e){var t,r=e.title;r||(t=p.getStateByIndex(0),t&&t.url===e.url&&(r=t.title||p.options.initialTitle));try{a.getElementsByTagName("title")[0].innerHTML=r.replace("<","<").replace(">",">").replace(" & "," & ")}catch(n){}return a.title=r,p},p.queues=[],p.busy=function(e){if("undefined"!=typeof e?p.busy.flag=e:"undefined"==typeof p.busy.flag&&(p.busy.flag=!1),!p.busy.flag){s(p.busy.timeout);var t=function(){var e,r,a;if(!p.busy.flag)for(e=p.queues.length-1;e>=0;--e)r=p.queues[e],0!==r.length&&(a=r.shift(),p.fireQueueItem(a),p.busy.timeout=i(t,p.options.busyDelay))};p.busy.timeout=i(t,p.options.busyDelay)}return p.busy.flag},p.busy.flag=!1,p.fireQueueItem=function(e){return e.callback.apply(e.scope||p,e.args||[])},p.pushQueue=function(e){return p.queues[e.queue||0]=p.queues[e.queue||0]||[],p.queues[e.queue||0].push(e),p},p.queue=function(e,t){return"function"==typeof e&&(e={callback:e}),"undefined"!=typeof t&&(e.queue=t),p.busy()?p.pushQueue(e):p.fireQueueItem(e),p},p.clearQueue=function(){return p.busy.flag=!1,p.queues=[],p},p.stateChanged=!1,p.doubleChecker=!1,p.doubleCheckComplete=function(){return p.stateChanged=!0,p.doubleCheckClear(),p},p.doubleCheckClear=function(){return p.doubleChecker&&(s(p.doubleChecker),p.doubleChecker=!1),p},p.doubleCheck=function(e){return p.stateChanged=!1,p.doubleCheckClear(),p.bugs.ieDoubleCheck&&(p.doubleChecker=i(function(){return p.doubleCheckClear(),p.stateChanged||e(),!0},p.options.doubleCheckInterval)),p},p.safariStatePoll=function(){var t,r=p.extractState(p.getLocationHref());return p.isLastSavedState(r)?void 0:(t=r,t||(t=p.createStateObject()),p.Adapter.trigger(e,"popstate"),p)},p.back=function(e){return e!==!1&&p.busy()?(p.pushQueue({scope:p,callback:p.back,args:arguments,queue:e}),!1):(p.busy(!0),p.doubleCheck(function(){p.back(!1)}),f.go(-1),!0)},p.forward=function(e){return e!==!1&&p.busy()?(p.pushQueue({scope:p,callback:p.forward,args:arguments,queue:e}),!1):(p.busy(!0),p.doubleCheck(function(){p.forward(!1)}),f.go(1),!0)},p.go=function(e,t){var r;if(e>0)for(r=1;e>=r;++r)p.forward(t);else{if(!(0>e))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(r=-1;r>=e;--r)p.back(t)}return p},p.emulated.pushState){var h=function(){};p.pushState=p.pushState||h,p.replaceState=p.replaceState||h}else p.onPopState=function(t,r){var a,n,o=!1,i=!1;return p.doubleCheckComplete(),a=p.getHash(),a?(n=p.extractState(a||p.getLocationHref(),!0),n?p.replaceState(n.data,n.title,n.url,!1):(p.Adapter.trigger(e,"anchorchange"),p.busy(!1)),p.expectedStateId=!1,!1):(o=p.Adapter.extractEventData("state",t,r)||!1,i=o?p.getStateById(o):p.expectedStateId?p.getStateById(p.expectedStateId):p.extractState(p.getLocationHref()),i||(i=p.createStateObject(null,null,p.getLocationHref())),p.expectedStateId=!1,p.isLastSavedState(i)?(p.busy(!1),!1):(p.storeState(i),p.saveState(i),p.setTitle(i),p.Adapter.trigger(e,"statechange"),p.busy(!1),!0))},p.Adapter.bind(e,"popstate",p.onPopState),p.pushState=function(t,r,a,n){if(p.getHashByUrl(a)&&p.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(n!==!1&&p.busy())return p.pushQueue({scope:p,callback:p.pushState,args:arguments,queue:n}),!1;p.busy(!0);var o=p.createStateObject(t,r,a);return p.isLastSavedState(o)?p.busy(!1):(p.storeState(o),p.expectedStateId=o.id,f.pushState(o.id,o.title,o.url),p.Adapter.trigger(e,"popstate")),!0},p.replaceState=function(t,r,a,n){if(p.getHashByUrl(a)&&p.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(n!==!1&&p.busy())return p.pushQueue({scope:p,callback:p.replaceState,args:arguments,queue:n}),!1;p.busy(!0);var o=p.createStateObject(t,r,a);return p.isLastSavedState(o)?p.busy(!1):(p.storeState(o),p.expectedStateId=o.id,f.replaceState(o.id,o.title,o.url),p.Adapter.trigger(e,"popstate")),!0};if(o){try{p.store=d.parse(o.getItem("History.store"))||{}}catch(S){p.store={}}p.normalizeStore()}else p.store={},p.normalizeStore();p.Adapter.bind(e,"unload",p.clearAllIntervals),p.saveState(p.storeState(p.extractState(p.getLocationHref(),!0))),o&&(p.onUnload=function(){var e,t,r;try{e=d.parse(o.getItem("History.store"))||{}}catch(a){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in p.idToState)p.idToState.hasOwnProperty(t)&&(e.idToState[t]=p.idToState[t]);for(t in p.urlToId)p.urlToId.hasOwnProperty(t)&&(e.urlToId[t]=p.urlToId[t]);for(t in p.stateToId)p.stateToId.hasOwnProperty(t)&&(e.stateToId[t]=p.stateToId[t]);p.store=e,p.normalizeStore(),r=d.stringify(e);try{o.setItem("History.store",r)}catch(n){if(n.code!==DOMException.QUOTA_EXCEEDED_ERR)throw n;o.length&&(o.removeItem("History.store"),o.setItem("History.store",r))}},p.intervalList.push(u(p.onUnload,p.options.storeInterval)),p.Adapter.bind(e,"beforeunload",p.onUnload),p.Adapter.bind(e,"unload",p.onUnload)),p.emulated.pushState||(p.bugs.safariPoll&&p.intervalList.push(u(p.safariStatePoll,p.options.safariPollInterval)),("Apple Computer, Inc."===n.vendor||"Mozilla"===(n.appCodeName||""))&&(p.Adapter.bind(e,"hashchange",function(){p.Adapter.trigger(e,"popstate")}),p.getHash()&&p.Adapter.onDomLoad(function(){p.Adapter.trigger(e,"hashchange")})))},(!p.options||!p.options.delayInit)&&p.init()}(window); +!function(e,t){"use strict";var r=e.History=e.History||{},a=e.jQuery;if("undefined"!=typeof r.Adapter)throw new Error("History.js Adapter has already been loaded...");r.Adapter={bind:function(e,t,r){a(e).bind(t,r)},trigger:function(e,t,r){a(e).trigger(t,r)},extractEventData:function(e,r,a){var n=r&&r.originalEvent&&r.originalEvent[e]||a&&a[e]||t;return n},onDomLoad:function(e){a(e)}},"undefined"!=typeof r.init&&r.init()}(window),function(e,t){"use strict";var r=e.console||t,a=e.document,n=e.navigator,o=e.sessionStorage||!1,i=e.setTimeout,s=e.clearTimeout,u=e.setInterval,l=e.clearInterval,d=e.JSON,c=e.alert,p=e.History=e.History||{},f=e.history;try{o.setItem("TEST","1"),o.removeItem("TEST")}catch(g){o=!1}if(d.stringify=d.stringify||d.encode,d.parse=d.parse||d.decode,"undefined"!=typeof p.init)throw new Error("History.js Core has already been loaded...");p.init=function(e){return"undefined"==typeof p.Adapter?!1:("undefined"!=typeof p.initCore&&p.initCore(),"undefined"!=typeof p.initHtml4&&p.initHtml4(),!0)},p.initCore=function(g){if("undefined"!=typeof p.initCore.initialized)return!1;if(p.initCore.initialized=!0,p.options=p.options||{},p.options.hashChangeInterval=p.options.hashChangeInterval||100,p.options.safariPollInterval=p.options.safariPollInterval||500,p.options.doubleCheckInterval=p.options.doubleCheckInterval||500,p.options.disableSuid=p.options.disableSuid||!1,p.options.storeInterval=p.options.storeInterval||1e3,p.options.busyDelay=p.options.busyDelay||250,p.options.debug=p.options.debug||!1,p.options.initialTitle=p.options.initialTitle||a.title,p.options.html4Mode=p.options.html4Mode||!1,p.options.delayInit=p.options.delayInit||!1,p.intervalList=[],p.clearAllIntervals=function(){var e,t=p.intervalList;if("undefined"!=typeof t&&null!==t){for(e=0;et;++t){if(i=arguments[t],"object"==typeof i&&"undefined"!=typeof d)try{i=d.stringify(i)}catch(l){}e+="\n"+i+"\n"}return u?(u.value+=e+"\n-----\n",u.scrollTop=u.scrollHeight-u.clientHeight):s||c(e),!0},p.getInternetExplorerMajorVersion=function(){var e=p.getInternetExplorerMajorVersion.cached="undefined"!=typeof p.getInternetExplorerMajorVersion.cached?p.getInternetExplorerMajorVersion.cached:function(){for(var e=3,t=a.createElement("div"),r=t.getElementsByTagName("i");(t.innerHTML="")&&r[0];);return e>4?e:!1}();return e},p.isInternetExplorer=function(){var e=p.isInternetExplorer.cached="undefined"!=typeof p.isInternetExplorer.cached?p.isInternetExplorer.cached:Boolean(p.getInternetExplorerMajorVersion());return e},p.options.html4Mode?p.emulated={pushState:!0,hashChange:!0}:p.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(n.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(n.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in a)||p.isInternetExplorer()&&p.getInternetExplorerMajorVersion()<8)},p.enabled=!p.emulated.pushState,p.bugs={setHash:Boolean(!p.emulated.pushState&&"Apple Computer, Inc."===n.vendor&&/AppleWebKit\/5([0-2]|3[0-3])/.test(n.userAgent)),safariPoll:Boolean(!p.emulated.pushState&&"Apple Computer, Inc."===n.vendor&&/AppleWebKit\/5([0-2]|3[0-3])/.test(n.userAgent)),ieDoubleCheck:Boolean(p.isInternetExplorer()&&p.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(p.isInternetExplorer()&&p.getInternetExplorerMajorVersion()<7)},p.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},p.cloneObject=function(e){var t,r;return e?(t=d.stringify(e),r=d.parse(t)):r={},r},p.getRootUrl=function(){var e=a.location.protocol+"//"+(a.location.hostname||a.location.host);return a.location.port&&(e+=":"+a.location.port),e+="/"},p.getBaseHref=function(){var e=a.getElementsByTagName("base"),t=null,r="";return 1===e.length&&(t=e[0],r=t.href.replace(/[^\/]+$/,"")),r=r.replace(/\/+$/,""),r&&(r+="/"),r},p.getBaseUrl=function(){var e=p.getBaseHref()||p.getBasePageUrl()||p.getRootUrl();return e},p.getPageUrl=function(){var e,t=p.getState(!1,!1),r=(t||{}).url||p.getLocationHref();return e=r.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,r){return/\./.test(e)?e:e+"/"})},p.getBasePageUrl=function(){var e=p.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,r){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},p.getFullUrl=function(e,t){var r=e,a=e.substring(0,1);return t="undefined"==typeof t?!0:t,/[a-z]+\:\/\//.test(e)||(r="/"===a?p.getRootUrl()+e.replace(/^\/+/,""):"#"===a?p.getPageUrl().replace(/#.*/,"")+e:"?"===a?p.getPageUrl().replace(/[\?#].*/,"")+e:t?p.getBaseUrl()+e.replace(/^(\.\/)+/,""):p.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),r.replace(/\#$/,"")},p.getShortUrl=function(e){var t=e,r=p.getBaseUrl(),a=p.getRootUrl();return p.emulated.pushState&&(t=t.replace(r,"")),t=t.replace(a,"/"),p.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,"")},p.getLocationHref=function(e){return e=e||a,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:-1==e.URL.indexOf("#")&&-1!=e.location.href.indexOf("#")?e.location.href:e.URL||e.location.href},p.store={},p.idToState=p.idToState||{},p.stateToId=p.stateToId||{},p.urlToId=p.urlToId||{},p.storedStates=p.storedStates||[],p.savedStates=p.savedStates||[],p.normalizeStore=function(){p.store.idToState=p.store.idToState||{},p.store.urlToId=p.store.urlToId||{},p.store.stateToId=p.store.stateToId||{}},p.getState=function(e,t){"undefined"==typeof e&&(e=!0),"undefined"==typeof t&&(t=!0);var r=p.getLastSavedState();return!r&&t&&(r=p.createStateObject()),e&&(r=p.cloneObject(r),r.url=r.cleanUrl||r.url),r},p.getIdByState=function(e){var t,r=p.extractId(e.url);if(!r)if(t=p.getStateString(e),"undefined"!=typeof p.stateToId[t])r=p.stateToId[t];else if("undefined"!=typeof p.store.stateToId[t])r=p.store.stateToId[t];else{for(;r=(new Date).getTime()+String(Math.random()).replace(/\D/g,""),"undefined"!=typeof p.idToState[r]||"undefined"!=typeof p.store.idToState[r];);p.stateToId[t]=r,p.idToState[r]=e}return r},p.normalizeState=function(e){var t,r;return e&&"object"==typeof e||(e={}),"undefined"!=typeof e.normalized?e:(e.data&&"object"==typeof e.data||(e.data={}),t={},t.normalized=!0,t.title=e.title||"",t.url=p.getFullUrl(e.url?e.url:p.getLocationHref()),t.hash=p.getShortUrl(t.url),t.data=p.cloneObject(e.data),t.id=p.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,r=!p.isEmptyObject(t.data),(t.title||r)&&p.options.disableSuid!==!0&&(t.hash=p.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=p.getFullUrl(t.hash),(p.emulated.pushState||p.bugs.safariPoll)&&p.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t)},p.createStateObject=function(e,t,r){var a={data:e,title:t,url:r};return a=p.normalizeState(a)},p.getStateById=function(e){e=String(e);var r=p.idToState[e]||p.store.idToState[e]||t;return r},p.getStateString=function(e){var t,r,a;return t=p.normalizeState(e),r={data:t.data,title:e.title,url:e.url},a=d.stringify(r)},p.getStateId=function(e){var t,r;return t=p.normalizeState(e),r=t.id},p.getHashByState=function(e){var t,r;return t=p.normalizeState(e),r=t.hash},p.extractId=function(e){var t,r,a,n;return n=-1!=e.indexOf("#")?e.split("#")[0]:e,r=/(.*)\&_suid=([0-9]+)$/.exec(n),a=r?r[1]||e:e,t=r?String(r[2]||""):"",t||!1},p.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},p.extractState=function(e,t){var r,a,n=null;return t=t||!1,r=p.extractId(e),r&&(n=p.getStateById(r)),n||(a=p.getFullUrl(e),r=p.getIdByUrl(a)||!1,r&&(n=p.getStateById(r)),!n&&t&&!p.isTraditionalAnchor(e)&&(n=p.createStateObject(null,null,a))),n},p.getIdByUrl=function(e){var r=p.urlToId[e]||p.store.urlToId[e]||t;return r},p.getLastSavedState=function(){return p.savedStates[p.savedStates.length-1]||t},p.getLastStoredState=function(){return p.storedStates[p.storedStates.length-1]||t},p.hasUrlDuplicate=function(e){var t,r=!1;return t=p.extractState(e.url),r=t&&t.id!==e.id},p.storeState=function(e){return p.urlToId[e.url]=e.id,p.storedStates.push(p.cloneObject(e)),e},p.isLastSavedState=function(e){var t,r,a,n=!1;return p.savedStates.length&&(t=e.id,r=p.getLastSavedState(),a=r.id,n=t===a),n},p.saveState=function(e){return p.isLastSavedState(e)?!1:(p.savedStates.push(p.cloneObject(e)),!0)},p.getStateByIndex=function(e){var t=null;return t="undefined"==typeof e?p.savedStates[p.savedStates.length-1]:0>e?p.savedStates[p.savedStates.length+e]:p.savedStates[e]},p.getCurrentIndex=function(){var e=null;return e=p.savedStates.length<1?0:p.savedStates.length-1},p.getHash=function(e){var t,r=p.getLocationHref(e);return t=p.getHashByUrl(r)},p.unescapeHash=function(e){var t=p.normalizeHash(e);return t=decodeURIComponent(t)},p.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},p.setHash=function(e,t){var r,n;return t!==!1&&p.busy()?(p.pushQueue({scope:p,callback:p.setHash,args:arguments,queue:t}),!1):(p.busy(!0),r=p.extractState(e,!0),r&&!p.emulated.pushState?p.pushState(r.data,r.title,r.url,!1):p.getHash()!==e&&(p.bugs.setHash?(n=p.getPageUrl(),p.pushState(null,null,n+"#"+e,!1)):a.location.hash=e),p)},p.escapeHash=function(t){var r=p.normalizeHash(t);return r=e.encodeURIComponent(r),p.bugs.hashEscape||(r=r.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),r},p.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=p.unescapeHash(t)},p.setTitle=function(e){var t,r=e.title;r||(t=p.getStateByIndex(0),t&&t.url===e.url&&(r=t.title||p.options.initialTitle));try{a.getElementsByTagName("title")[0].innerHTML=r.replace("<","<").replace(">",">").replace(" & "," & ")}catch(n){}return a.title=r,p},p.queues=[],p.busy=function(e){if("undefined"!=typeof e?p.busy.flag=e:"undefined"==typeof p.busy.flag&&(p.busy.flag=!1),!p.busy.flag){s(p.busy.timeout);var t=function(){var e,r,a;if(!p.busy.flag)for(e=p.queues.length-1;e>=0;--e)r=p.queues[e],0!==r.length&&(a=r.shift(),p.fireQueueItem(a),p.busy.timeout=i(t,p.options.busyDelay))};p.busy.timeout=i(t,p.options.busyDelay)}return p.busy.flag},p.busy.flag=!1,p.fireQueueItem=function(e){return e.callback.apply(e.scope||p,e.args||[])},p.pushQueue=function(e){return p.queues[e.queue||0]=p.queues[e.queue||0]||[],p.queues[e.queue||0].push(e),p},p.queue=function(e,t){return"function"==typeof e&&(e={callback:e}),"undefined"!=typeof t&&(e.queue=t),p.busy()?p.pushQueue(e):p.fireQueueItem(e),p},p.clearQueue=function(){return p.busy.flag=!1,p.queues=[],p},p.stateChanged=!1,p.doubleChecker=!1,p.doubleCheckComplete=function(){return p.stateChanged=!0,p.doubleCheckClear(),p},p.doubleCheckClear=function(){return p.doubleChecker&&(s(p.doubleChecker),p.doubleChecker=!1),p},p.doubleCheck=function(e){return p.stateChanged=!1,p.doubleCheckClear(),p.bugs.ieDoubleCheck&&(p.doubleChecker=i(function(){return p.doubleCheckClear(),p.stateChanged||e(),!0},p.options.doubleCheckInterval)),p},p.safariStatePoll=function(){var t,r=p.extractState(p.getLocationHref());return p.isLastSavedState(r)?void 0:(t=r,t||(t=p.createStateObject()),p.Adapter.trigger(e,"popstate"),p)},p.back=function(e){return e!==!1&&p.busy()?(p.pushQueue({scope:p,callback:p.back,args:arguments,queue:e}),!1):(p.busy(!0),p.doubleCheck(function(){p.back(!1)}),f.go(-1),!0)},p.forward=function(e){return e!==!1&&p.busy()?(p.pushQueue({scope:p,callback:p.forward,args:arguments,queue:e}),!1):(p.busy(!0),p.doubleCheck(function(){p.forward(!1)}),f.go(1),!0)},p.go=function(e,t){var r;if(e>0)for(r=1;e>=r;++r)p.forward(t);else{if(!(0>e))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(r=-1;r>=e;--r)p.back(t)}return p},p.emulated.pushState){var h=function(){};p.pushState=p.pushState||h,p.replaceState=p.replaceState||h}else p.onPopState=function(t,r){var a,n,o=!1,i=!1;return p.doubleCheckComplete(),a=p.getHash(),a?(n=p.extractState(a||p.getLocationHref(),!0),n?p.replaceState(n.data,n.title,n.url,!1):(p.Adapter.trigger(e,"anchorchange"),p.busy(!1)),p.expectedStateId=!1,!1):(o=p.Adapter.extractEventData("state",t,r)||!1,i=o?p.getStateById(o):p.expectedStateId?p.getStateById(p.expectedStateId):p.extractState(p.getLocationHref()),i||(i=p.createStateObject(null,null,p.getLocationHref())),p.expectedStateId=!1,p.isLastSavedState(i)?(p.busy(!1),!1):(p.storeState(i),p.saveState(i),p.setTitle(i),p.Adapter.trigger(e,"statechange"),p.busy(!1),!0))},p.Adapter.bind(e,"popstate",p.onPopState),p.pushState=function(t,r,a,n){if(p.getHashByUrl(a)&&p.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(n!==!1&&p.busy())return p.pushQueue({scope:p,callback:p.pushState,args:arguments,queue:n}),!1;p.busy(!0);var o=p.createStateObject(t,r,a);return p.isLastSavedState(o)?p.busy(!1):(p.storeState(o),p.expectedStateId=o.id,f.pushState(o.id,o.title,o.url),p.Adapter.trigger(e,"popstate")),!0},p.replaceState=function(t,r,a,n){if(p.getHashByUrl(a)&&p.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(n!==!1&&p.busy())return p.pushQueue({scope:p,callback:p.replaceState,args:arguments,queue:n}),!1;p.busy(!0);var o=p.createStateObject(t,r,a);return p.isLastSavedState(o)?p.busy(!1):(p.storeState(o),p.expectedStateId=o.id,f.replaceState(o.id,o.title,o.url),p.Adapter.trigger(e,"popstate")),!0};if(o){try{p.store=d.parse(o.getItem("History.store"))||{}}catch(S){p.store={}}p.normalizeStore()}else p.store={},p.normalizeStore();p.Adapter.bind(e,"unload",p.clearAllIntervals),p.saveState(p.storeState(p.extractState(p.getLocationHref(),!0))),o&&(p.onUnload=function(){var e,t,r;try{e=d.parse(o.getItem("History.store"))||{}}catch(a){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in p.idToState)p.idToState.hasOwnProperty(t)&&(e.idToState[t]=p.idToState[t]);for(t in p.urlToId)p.urlToId.hasOwnProperty(t)&&(e.urlToId[t]=p.urlToId[t]);for(t in p.stateToId)p.stateToId.hasOwnProperty(t)&&(e.stateToId[t]=p.stateToId[t]);p.store=e,p.normalizeStore(),r=d.stringify(e);try{o.setItem("History.store",r)}catch(n){if(n.code!==DOMException.QUOTA_EXCEEDED_ERR)throw n;o.length&&(o.removeItem("History.store"),o.setItem("History.store",r))}},p.intervalList.push(u(p.onUnload,p.options.storeInterval)),p.Adapter.bind(e,"beforeunload",p.onUnload),p.Adapter.bind(e,"unload",p.onUnload)),p.emulated.pushState||(p.bugs.safariPoll&&p.intervalList.push(u(p.safariStatePoll,p.options.safariPollInterval)),("Apple Computer, Inc."===n.vendor||"Mozilla"===(n.appCodeName||""))&&(p.Adapter.bind(e,"hashchange",function(){p.Adapter.trigger(e,"popstate")}),p.getHash()&&p.Adapter.onDomLoad(function(){p.Adapter.trigger(e,"hashchange")})))},(!p.options||!p.options.delayInit)&&p.init()}(window); },{}],17:[function(require,module,exports){ L.Map.mergeOptions({contextmenuItems:[]}),L.Map.ContextMenu=L.Handler.extend({statics:{BASE_CLS:"leaflet-contextmenu"},initialize:function(t){L.Handler.prototype.initialize.call(this,t),this._items=[],this._sets=[],this._state=0,this._defaultState=t.options.contextmenuDefaultState||1,this._activeState=t.options.contextmenuAtiveState||1,this._visible=!1;var e=this._container=L.DomUtil.create("div",L.Map.ContextMenu.BASE_CLS,t._container);e.style.zIndex=1e4,e.style.position="absolute",t.options.contextmenuWidth&&(e.style.width=t.options.contextmenuWidth+"px"),(void 0===t.options.contextmenuSets||0===t.options.contextmenuSets.length)&&(t.options.contextmenuSets=[{name:"set_default",state:this._defaultState}]),this._createItems(),this._createSets(),this._changeState(),L.DomEvent.on(e,"click",L.DomEvent.stop).on(e,"mousedown",L.DomEvent.stop).on(e,"dblclick",L.DomEvent.stop).on(e,"contextmenu",L.DomEvent.stop)},addHooks:function(){L.DomEvent.on(document,L.Browser.touch?"touchstart":"mousedown",this._onMouseDown,this).on(document,"keydown",this._onKeyDown,this),this._map.on({contextmenu:this._show,mouseout:this._hide,mousedown:this._hide,movestart:this._hide,zoomstart:this._hide},this)},removeHooks:function(){L.DomEvent.off(document,"keydown",this._onKeyDown,this),this._map.off({contextmenu:this._show,mouseout:this._hide,mousedown:this._hide,movestart:this._hide,zoomstart:this._hide},this)},showAt:function(t,e,i){t instanceof L.LatLng&&(t=this._map.latLngToContainerPoint(t)),this._showAtPoint(t,e,i)},hide:function(){this._hide()},setState:function(t){return this._changeState(t)},setActiveState:function(t){var e,i,n,t=void 0!==t?t:this._activeState;for(i=0,n=this._sets.length;n>i;i++)if(e=this._sets[i],e.state===t){this._activeState=t;break}return e},getState:function(){return this._state},addSet:function(t){return this.insertSet(t)},insertSet:function(t,e){var e=void 0!==e?e:this._sets.length,i=this._createSet(t,e);return this._sets.push(i),i},addItem:function(t){return this.insertItem(t)},insertItem:function(t,e){var e=void 0!==e?e:this._items.length,i=this._createItem(this._container,t,e);return this._items.push(i),this._sizeChanged=!0,this._map.fire("contextmenu.additem",{contextmenu:this,el:i.el,index:e}),i.el},removeItem:function(t){var e=this._container;isNaN(t)||(t=e.children[t]),void 0!==t&&(this._removeItem(L.Util.stamp(t)),this._sizeChanged=!0,this._map.fire("contextmenu.removeitem",{contextmenu:this,el:t}))},removeAllItems:function(){for(var t;this._container.children.length;)t=this._container.children[0],this._removeItem(L.Util.stamp(t))},setDisabled:function(t,e){var i=this._container,n=L.Map.ContextMenu.BASE_CLS+"-item";isNaN(t)||(t=i.children[t]),void 0!==t&&L.DomUtil.hasClass(t,n)&&(e?(L.DomUtil.addClass(t,n+"-disabled"),this._map.fire("contextmenu.disableitem",{contextmenu:this,el:t})):(L.DomUtil.removeClass(t,n+"-disabled"),this._map.fire("contextmenu.enableitem",{contextmenu:this,el:t})))},setHidden:function(t,e){var i=this._container,n=L.Map.ContextMenu.BASE_CLS+"-item",s=L.Map.ContextMenu.BASE_CLS+"-separator";isNaN(t)||(t=i.children[t]),void 0!==t&&L.DomUtil.hasClass(t,n)?e?(L.DomUtil.addClass(t,n+"-hidden"),this._map.fire("contextmenu.hideitem",{contextmenu:this,el:t})):(L.DomUtil.removeClass(t,n+"-hidden"),this._map.fire("contextmenu.showitem",{contextmenu:this,el:t})):void 0!==t&&L.DomUtil.hasClass(t,s)&&(e?L.DomUtil.addClass(t,s+"-hidden"):L.DomUtil.removeClass(t,s+"-hidden"))},isVisible:function(){return this._visible},_changeState:function(t){var e,i,n,s,t=void 0!==t?t:this._defaultState;if(t!==this._state)for(n=0,s=this._sets.length;s>n;n++)if(e=this._sets[n],e.state===t||e.name===t&&e.state!==this._state){for(this._map.fire("contextmenu.changestate",{contextmenu:this,set:e,state:t}),n=0,s=this._items.length;s>n;n++)i=this._items[n],this.setHidden(this._items[n].el,-1===i.state.indexOf(e.state)&&-1===i.state.indexOf(e.name));this._sizeChanged=!0,this._state=t;break}return e},_createSets:function(){var t,e,i=this._map.options.contextmenuSets;for(t=0,e=i.length;e>t;t++)this._sets.push(this._createSet(i[t],this._sets.length))},_createSet:function(t,e){void 0!==t.name?t.name:"set_"+e;return{id:e,name:t.name,state:t.state}},_createItems:function(){var t,e,i=this._map.options.contextmenuItems;for(t=0,e=i.length;e>t;t++)this._items.push(this._createItem(this._container,i[t]))},_createItem:function(t,e,i){if(e.separator||"-"===e)return this._createSeparator(t,i,e.state);var n=L.Map.ContextMenu.BASE_CLS+"-item",s=void 0!==e.state?Array.isArray(e.state)?e.state:[e.state]:[this._defaultState],o=e.disabled?n+" "+n+"-disabled":e.hidden?n+" "+n+"-hidden":n,a=this._insertElementAt("a",o,t,i),h=this._createEventHandler(a,e.callback,e.context,e.hideOnSelect),m="";return e.icon?m='':e.iconCls&&(m=''),a.innerHTML=m+e.text,a.href="#",L.DomEvent.on(a,"mouseover",this._onItemMouseOver,this).on(a,"mouseout",this._onItemMouseOut,this).on(a,"mousedown",L.DomEvent.stopPropagation).on(a,"click",h),{id:L.Util.stamp(a),el:a,callback:h,state:s}},_removeItem:function(t){var e,i,n,s,o;for(s=0,o=this._items.length;o>s;s++)if(e=this._items[s],e.id===t)return n=e.el,i=e.callback,i&&L.DomEvent.off(n,"mouseover",this._onItemMouseOver,this).off(n,"mouseover",this._onItemMouseOut,this).off(n,"mousedown",L.DomEvent.stopPropagation).off(n,"click",e.callback),this._container.removeChild(n),this._items.splice(s,1),e;return null},_createSeparator:function(t,e,i){var n=this._insertElementAt("div",L.Map.ContextMenu.BASE_CLS+"-separator",t,e),i=void 0!==i?Array.isArray(i)?i:[i]:[this._defaultState];return{id:L.Util.stamp(n),el:n,state:i}},_createEventHandler:function(t,e,i,n){var s=this,o=this._map,a=L.Map.ContextMenu.BASE_CLS+"-item-disabled",n=void 0!==n?n:!0;return function(h){L.DomUtil.hasClass(t,a)||(n&&s._hide(),e&&e.call(i||o,s._showLocation),s._map.fire("contextmenu:select",{contextmenu:s,el:t}))}},_insertElementAt:function(t,e,i,n){var s,o=document.createElement(t);return o.className=e,void 0!==n&&(s=i.children[n]),s?i.insertBefore(o,s):i.appendChild(o),o},_show:function(t){this._showAtPoint(t.containerPoint)},_showAtPoint:function(t,e,i){if(this._items.length){var n=this._map,s=n.containerPointToLayerPoint(t),o=n.layerPointToLatLng(s),a={contextmenu:this,state:i},i=void 0!==i?i:this._activeState;e&&(a=L.extend(e,a)),this._showLocation={state:i,target:e?e.relatedTarget:null,latlng:o,layerPoint:s,containerPoint:t},this._setPosition(t),this._changeState(i),this._visible?this._setPosition(t):(this._container.style.display="block",this._visible=!0),this._map.fire("contextmenu.show",a)}},_hide:function(){this._visible&&(this.setState(this._defaultState),this._visible=!1,this._container.style.display="none",this._map.fire("contextmenu.hide",{contextmenu:this}))},_setPosition:function(t){var e,i=this._map.getSize(),n=this._container,s=this._getElementSize(n);this._map.options.contextmenuAnchor&&(e=L.point(this._map.options.contextmenuAnchor),t=t.add(e)),n._leaflet_pos=t,t.x+s.x>i.x?(n.style.left="auto",n.style.right=Math.max(i.x-t.x,0)+"px"):(n.style.left=Math.max(t.x,0)+"px",n.style.right="auto"),t.y+s.y>i.y?(n.style.top="auto",n.style.bottom=Math.max(i.y-t.y,0)+"px"):(n.style.top=Math.max(t.y,0)+"px",n.style.bottom="auto")},_getElementSize:function(t){var e=this._size,i=t.style.display;return(!e||this._sizeChanged)&&(e={},t.style.left="-999999px",t.style.right="auto",t.style.display="block",e.x=t.offsetWidth,e.y=t.offsetHeight,t.style.left="auto",t.style.display=i,this._sizeChanged=!1),e},_onMouseDown:function(t){this._hide()},_onKeyDown:function(t){var e=t.keyCode;27===e&&this._hide()},_onItemMouseOver:function(t){L.DomUtil.addClass(t.target,"over")},_onItemMouseOut:function(t){L.DomUtil.removeClass(t.target,"over")}}),L.Map.addInitHook("addHandler","contextmenu",L.Map.ContextMenu),L.Mixin.ContextMenu={_initContextMenu:function(){this._items=[],this.on("contextmenu",this._showContextMenu,this)},_showContextMenu:function(t){var e,i,n,s;if(this._map.contextmenu){for(i=this._map.mouseEventToContainerPoint(t.originalEvent),n=0,s=this.options.contextmenuItems.length;s>n;n++)e=this.options.contextmenuItems[n],this._items.push(this._map.contextmenu.insertItem(e,e.index));this._map.once("contextmenu.hide",this._hideContextMenu,this),this._map.contextmenu.showAt(i,{relatedTarget:this},this.options.contextmenuAtiveState)}},_hideContextMenu:function(){var t,e;for(t=0,e=this._items.length;e>t;t++)this._map.contextmenu.removeItem(this._items[t]);this._items.length=0}},L.Marker.mergeOptions({contextmenu:!1,contextmenuItems:[]}),L.Marker.addInitHook(function(){this.options.contextmenu&&this._initContextMenu()}),L.Marker.include(L.Mixin.ContextMenu),L.Path.mergeOptions({contextmenu:!1,contextmenuItems:[]}),L.Path.addInitHook(function(){this.options.contextmenu&&this._initContextMenu()}),L.Path.include(L.Mixin.ContextMenu); @@ -70,10 +70,10 @@ L.NumberedDivIcon=L.Icon.extend({options:{iconUrl:"./img/marker_hole.png",number },{}],19:[function(require,module,exports){ (function (global){ -function initFromParams(e,t){ghRequest.init(e);var o,r=0;if(e.point)for(var n=0;n=2;a?resolveCoords(e.point,t):e.point&&1===r&&(ghRequest.route.set(e.point[o],o,!0),resolveIndex(o).done(function(){mapLayer.focus(ghRequest.route.getIndex(o),15,o)}))}function resolveCoords(e,t){for(var o=0,r=e.length;r>o;o++){var n=e[o],a=ghRequest.route.getIndex(o);a&&n===a.input&&a.isResolved()||ghRequest.route.set(n,o,!0)}checkInput(),ghRequest.route.isResolved()?(resolveAll(),routeLatLng(ghRequest,t)):$.when.apply($,resolveAll()).done(function(){routeLatLng(ghRequest,t)})}function getToFrom(e){return 0===e?FROM:e===ghRequest.route.size()-1?TO:-1}function checkInput(){var e=$("#pointTemplate").html(),t=ghRequest.route.size();$("#locationpoints > div.pointDiv").length>t&&$("#locationpoints > div.pointDiv:gt("+(t-1)+")").remove(),$("#locationpoints .pointDelete").off();for(var o=function(){var e=$(this).parent().data("index");ghRequest.route.removeSingle(e),mapLayer.clearLayers(),routeLatLng(ghRequest,!1)},r=0;t>r;r++){var n=$("#locationpoints > div.pointDiv").eq(r);0===n.length&&($("#locationpoints > div.pointAdd").before(translate.nanoTemplate(e,{id:r})),n=$("#locationpoints > div.pointDiv").eq(r));var a=getToFrom(r);if(n.data("index",r),n.find(".pointFlag").attr("src",a===FROM?"img/marker-small-green.png":a===TO?"img/marker-small-red.png":"img/marker-small-blue.png"),t>2?n.find(".pointDelete").click(o).show():n.find(".pointDelete").hide(),autocomplete.showListForIndex(ghRequest,routeIfAllResolved,r),translate.isI18nIsInitialized()){var s=n.find(".pointInput");0===r?$(s).attr("placeholder",translate.tr("fromHint")):r===t-1?$(s).attr("placeholder",translate.tr("toHint")):$(s).attr("placeholder",translate.tr("viaHint"))}}mapLayer.adjustMapSize()}function setToStart(e){var t=e.target.getLatLng(),o=ghRequest.route.getIndexByCoord(t);ghRequest.route.move(o,0),routeIfAllResolved()}function setToEnd(e){var t=e.target.getLatLng(),o=ghRequest.route.getIndexByCoord(t);ghRequest.route.move(o,-1),routeIfAllResolved()}function setStartCoord(e){ghRequest.route.set(e.latlng,0),resolveFrom(),routeIfAllResolved()}function setIntermediateCoord(e){var t=ghRequest.route.size()-1;ghRequest.route.add(e.latlng,t),resolveIndex(t),routeIfAllResolved()}function deleteCoord(e){var t=e.target.getLatLng();ghRequest.route.removeSingle(t),mapLayer.clearLayers(),routeLatLng(ghRequest,!1)}function setEndCoord(e){var t=ghRequest.route.size()-1;ghRequest.route.set(e.latlng,t),resolveTo(),routeIfAllResolved()}function routeIfAllResolved(e){return ghRequest.route.isResolved()?(routeLatLng(ghRequest,e),!0):!1}function setFlag(e,t){if(e.lat){var o=getToFrom(t),r=mapLayer.createMarker(t,e,setToEnd,setToStart,deleteCoord,ghRequest);r._openPopup=r.openPopup,r.openPopup=function(){var e,t=this.getLatLng(),o=ghRequest.route.getIndexFromCoord(t);if(o.resolvedList&&o.resolvedList[0]&&o.resolvedList[0].locationDetails){var r=o.resolvedList[0].locationDetails;e=format.formatAddress(r),this._popup.setContent(e).update()}this._openPopup()};var n={text:"Set as Start",callback:setToStart,index:1,state:2};-1===o&&r.options.contextmenuItems.push(n),r.on("dragend",function(e){mapLayer.clearLayers();var o=e.target.getLatLng();autocomplete.hide(),ghRequest.route.getIndex(t).setCoord(o.lat,o.lng),resolveIndex(t),ghRequest.do_zoom=!1,routeLatLng(ghRequest,!1)})}}function resolveFrom(){return resolveIndex(0)}function resolveTo(){return resolveIndex(ghRequest.route.size()-1)}function resolveIndex(e){return setFlag(ghRequest.route.getIndex(e),e),0===e?ghRequest.to.isResolved()?mapLayer.setDisabledForMapsContextMenu("start",!1):mapLayer.setDisabledForMapsContextMenu("start",!0):e===ghRequest.route.size()-1&&(ghRequest.from.isResolved()?mapLayer.setDisabledForMapsContextMenu("end",!1):mapLayer.setDisabledForMapsContextMenu("end",!0)),nominatim.resolve(e,ghRequest.route.getIndex(e))}function resolveAll(){for(var e=[],t=0,o=ghRequest.route.size();o>t;t++)e[t]=resolveIndex(t);return e}function flagAll(){for(var e=0,t=ghRequest.route.size();t>e;e++)setFlag(ghRequest.route.getIndex(e),e)}function routeLatLng(e,t){var o=e.do_zoom;e.do_zoom=!0;var r=e.createHistoryURL()+"&layer="+tileLayers.activeLayerName;if(!t&&History.enabled){var n=urlTools.parseUrl(r);return log(n),n.do_zoom=o,n.mathRandom=Math.random(),void History.pushState(n,messages.browserTitle,r)}$("#info").empty(),$("#info").show();var a=$("
");$("#info").append(a),mapLayer.clearElevation(),mapLayer.clearLayers(),flagAll(),mapLayer.setDisabledForMapsContextMenu("intermediate",!1),$("#vehicles button").removeClass("selectvehicle"),$("button#"+e.getVehicle().toLowerCase()).addClass("selectvehicle");var s=e.createURL();a.html(' Search Route ...'),e.doRequest(s,function(t){if(a.html(""),t.message){var n=t.message;if(log(n),t.hints)for(var s=0;s"+t.hints[s].message+"
");else a.append("
"+n+"
")}else{var i=t.paths[0],l={type:"Feature",geometry:i.points};if(e.hasElevation()&&mapLayer.addElevation(l),mapLayer.addDataToRoutingLayer(l),i.bbox&&o){var u=i.bbox[0],d=i.bbox[1],p=i.bbox[2],g=i.bbox[3],c=new L.LatLngBounds(new L.LatLng(d,u),new L.LatLng(g,p));mapLayer.fitMapToBounds(c)}var h=translate.createTimeString(i.time),v=translate.createDistanceString(i.distance),m="";if(e.hasElevation()&&(m=translate.createEleInfoString(i.ascend,i.descend)),a.append(translate.tr("routeInfo",[v,h])),a.append(m),$(".defaulting").each(function(e,t){$(t).css("color","black")}),i.instructions){var f=require("./instructions.js");f.addInstructions(mapLayer,i,r,e)}}})}function mySubmit(){var e,t,o,r=[],n=!0,a=$("#locationpoints > div.pointDiv > input.pointInput"),s=a.size();return $.each(a,function(a){0===a?(e=$(this).val(),e!==translate.tr("fromHint")&&""!==e?r.push(e):n=!1):a===s-1?(t=$(this).val(),t!==translate.tr("toHint")&&""!==t?r.push(t):n=!1):(o=$(this).val(),o!==translate.tr("viaHint")&&""!==o?r.push(o):n=!1)}),n&&e!==translate.tr("fromHint")?t===translate.tr("toHint")?(ghRequest.from.setStr(e),void $.when(resolveFrom()).done(function(){mapLayer.focus(ghRequest.from,null,0)})):void(n&&resolveCoords(r)):void 0}function isProduction(){return host.indexOf("graphhopper.com")>0}global.d3=require("d3");var L=require("leaflet");require("leaflet-loading"),require("./lib/leaflet.contextmenu.js"),require("./lib/Leaflet.Elevation-0.0.2.min.js"),require("./lib/leaflet_numbered_markers.js"),global.jQuery=require("jquery"),global.$=global.jQuery,require("./lib/jquery-ui-custom-1.11.4.min.js"),require("./lib/jquery.history.js"),require("./lib/jquery.autocomplete.js");var ghenv=require("./options.js").options;console.log(ghenv.environment);var GHInput=require("./graphhopper/GHInput.js"),GHRequest=require("./graphhopper/GHRequest.js"),host=ghenv.routing.host;host||(host=""===location.port?location.protocol+"//"+location.hostname:location.protocol+"//"+location.hostname+":"+location.port);var AutoComplete=require("./autocomplete.js");if("development"===ghenv.environment)var autocomplete=AutoComplete.prototype.createStub();else var autocomplete=new AutoComplete(ghenv.geocoding.host,ghenv.geocoding.api_key);var mapLayer=require("./map.js"),nominatim=require("./nominatim.js"),gpxExport=require("./gpxexport.js"),messages=require("./messages.js"),translate=require("./translate.js"),format=require("./tools/format.js"),urlTools=require("./tools/url.js"),vehicle=require("./tools/vehicle.js"),tileLayers=require("./config/tileLayers.js"),debug=!1,ghRequest=new GHRequest(host,ghenv.routing.api_key),bounds={},metaVersionInfo;global.window&&(window.log=function(){log.history=log.history||[],log.history.push(arguments),this.console&&debug&&console.log(Array.prototype.slice.call(arguments))}),$(document).ready(function(e){jQuery.support.cors=!0,gpxExport.addGpxExport(ghRequest),isProduction()&&$("#hosting").show();var t=window.History;t.enabled&&t.Adapter.bind(window,"statechange",function(){var e=t.getState();log(e),initFromParams(e.data,!0)}),$("#locationform").submit(function(e){e.preventDefault(),mySubmit()});var o=urlTools.parseUrlWithHisto();$.when(ghRequest.fetchTranslationMap(o.locale),ghRequest.getInfo()).then(function(e,t){function r(e,t){var o=$("