diff --git a/isochrone/src/main/java/com/graphhopper/isochrone/algorithm/Isochrone.java b/isochrone/src/main/java/com/graphhopper/isochrone/algorithm/Isochrone.java index 7e6c1e5eeed..4019dcb9174 100644 --- a/isochrone/src/main/java/com/graphhopper/isochrone/algorithm/Isochrone.java +++ b/isochrone/src/main/java/com/graphhopper/isochrone/algorithm/Isochrone.java @@ -103,6 +103,49 @@ public void setDistanceLimit(double limit) { this.finishLimit = limit + Math.max(limit * 0.14, 2_000); } + public static class IsoLabelWithCoordinates { + public Coordinate baseCoordinate; + public Coordinate adjCoordinate; + public long time; + public double distance; + // debug info + public int edgeId, baseNodeId; + public final int adjNodeId; + + public IsoLabelWithCoordinates(int adjNodeId) { + this.adjNodeId = adjNodeId; + } + } + + public List search(int from) { + searchInternal(from); + + final List shortestPathEntries = new ArrayList<>(fromMap.size()); + final NodeAccess na = graph.getNodeAccess(); + fromMap.forEach(new IntObjectProcedure() { + + @Override + public void apply(int nodeId, IsoLabel label) { + double lat = na.getLatitude(nodeId); + double lon = na.getLongitude(nodeId); + IsoLabelWithCoordinates sptInfo = new IsoLabelWithCoordinates(nodeId); + sptInfo.adjCoordinate = new Coordinate(lon, lat); + sptInfo.time = label.time; + sptInfo.distance = label.distance; + sptInfo.edgeId = label.edge; + shortestPathEntries.add(sptInfo); + if (label.parent != null) { + nodeId = label.parent.adjNode; + double lat2 = na.getLatitude(nodeId); + double lon2 = na.getLongitude(nodeId); + sptInfo.baseNodeId = nodeId; + sptInfo.baseCoordinate = new Coordinate(lon2, lat2); + } + } + }); + return shortestPathEntries; + } + public List> searchGPS(int from, final int bucketCount) { searchInternal(from); diff --git a/web-bundle/src/main/java/com/graphhopper/resources/IsochroneResource.java b/web-bundle/src/main/java/com/graphhopper/resources/IsochroneResource.java index 4496fa7f4d6..d14d5eb1520 100644 --- a/web-bundle/src/main/java/com/graphhopper/resources/IsochroneResource.java +++ b/web-bundle/src/main/java/com/graphhopper/resources/IsochroneResource.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.graphhopper.GraphHopper; -import com.graphhopper.isochrone.algorithm.Isochrone; import com.graphhopper.isochrone.algorithm.DelaunayTriangulationIsolineBuilder; +import com.graphhopper.isochrone.algorithm.Isochrone; import com.graphhopper.json.geo.JsonFeature; import com.graphhopper.routing.QueryGraph; import com.graphhopper.routing.util.*; @@ -12,6 +12,7 @@ import com.graphhopper.storage.Graph; import com.graphhopper.storage.index.LocationIndex; import com.graphhopper.storage.index.QueryResult; +import com.graphhopper.util.Helper; import com.graphhopper.util.StopWatch; import com.graphhopper.util.shapes.GHPoint; import org.locationtech.jts.geom.Coordinate; @@ -55,6 +56,7 @@ public Response doGet( @QueryParam("reverse_flow") @DefaultValue("false") boolean reverseFlow, @QueryParam("point") GHPoint point, @QueryParam("result") @DefaultValue("polygon") String resultStr, + @QueryParam("pointlist_ext_header") String extendedHeader, @QueryParam("time_limit") @DefaultValue("600") long timeLimitInSeconds, @QueryParam("distance_limit") @DefaultValue("-1") double distanceInMeter) { @@ -92,28 +94,21 @@ public Response doGet( isochrone.setTimeLimit(timeLimitInSeconds); } - List> buckets = isochrone.searchGPS(qr.getClosestNode(), nBuckets); - if (isochrone.getVisitedNodes() > graphHopper.getMaxVisitedNodes() / 5) { - throw new IllegalArgumentException("Server side reset: too many junction nodes would have to explored (" + isochrone.getVisitedNodes() + "). Let us know if you need this increased."); - } - - int counter = 0; - for (List bucket : buckets) { - if (bucket.size() < 2) { - throw new IllegalArgumentException("Too few points found for bucket " + counter + ". " - + "Please try a different 'point', a smaller 'buckets' count or a larger 'time_limit'. " - + "And let us know if you think this is a bug!"); + if ("polygon".equalsIgnoreCase(resultStr)) { + List> buckets = isochrone.searchGPS(qr.getClosestNode(), nBuckets); + if (isochrone.getVisitedNodes() > graphHopper.getMaxVisitedNodes() / 5) { + throw new IllegalArgumentException("Server side reset: too many junction nodes would have to explored (" + isochrone.getVisitedNodes() + "). Let us know if you need this increased."); } - counter++; - } - if ("pointlist".equalsIgnoreCase(resultStr)) { - sw.stop(); - logger.info("took: " + sw.getSeconds() + ", visited nodes:" + isochrone.getVisitedNodes() + ", " + uriInfo.getQueryParameters()); - return Response.fromResponse(jsonSuccessResponse(buckets, sw.getSeconds())) - .header("X-GH-Took", "" + sw.getSeconds() * 1000) - .build(); - } else if ("polygon".equalsIgnoreCase(resultStr)) { + int counter = 0; + for (List bucket : buckets) { + if (bucket.size() < 2) { + throw new IllegalArgumentException("Too few points found for bucket " + counter + ". " + + "Please try a different 'point', a smaller 'buckets' count or a larger 'time_limit'. " + + "And let us know if you think this is a bug!"); + } + counter++; + } ArrayList features = new ArrayList<>(); List polygonShells = delaunayTriangulationIsolineBuilder.calcList(buckets, buckets.size() - 1); for (Coordinate[] polygonShell : polygonShells) { @@ -126,17 +121,70 @@ public Response doGet( } sw.stop(); logger.info("took: " + sw.getSeconds() + ", visited nodes:" + isochrone.getVisitedNodes() + ", " + uriInfo.getQueryParameters()); - return Response.fromResponse(jsonSuccessResponse(features, sw.getSeconds())) + return Response.fromResponse(jsonSuccessResponse("polygons", features, sw.getSeconds())) + .header("X-GH-Took", "" + sw.getSeconds() * 1000) + .build(); + } else if ("pointlist".equalsIgnoreCase(resultStr)) { + Collection header = new LinkedHashSet<>(Arrays.asList("longitude", "latitude", "time", "distance")); + if (!Helper.isEmpty(extendedHeader)) + header.addAll(Arrays.asList(extendedHeader.split(","))); + List resultList = isochrone.search(qr.getClosestNode()); + List items = new ArrayList(resultList.size()); + for (Isochrone.IsoLabelWithCoordinates label : resultList) { + List list = new ArrayList(header.size()); + for (String h : header) { + switch (h) { + case "distance": + list.add(label.distance); + break; + case "time": + list.add(label.distance); + break; + case "node_id": + list.add(label.adjNodeId); + break; + case "edge_id": + list.add(label.edgeId); + break; + case "longitude": + list.add(label.adjCoordinate.x); + break; + case "latitude": + list.add(label.adjCoordinate.y); + break; + case "prev_longitude": + list.add(label.baseCoordinate == null ? null : label.baseCoordinate.x); + break; + case "prev_latitude": + list.add(label.baseCoordinate == null ? null : label.baseCoordinate.y); + break; + case "prev_node_id": + list.add(label.baseNodeId); + break; + default: + throw new IllegalArgumentException("Unknown property " + h); + } + } + items.add(list); + } + sw.stop(); + Map map = new HashMap(); + map.put("header", header); + map.put("items", items); + + logger.info("took: " + sw.getSeconds() + ", visited nodes:" + isochrone.getVisitedNodes() + ", " + uriInfo.getQueryParameters()); + return Response.fromResponse(jsonSuccessResponse("result", map, sw.getSeconds())) .header("X-GH-Took", "" + sw.getSeconds() * 1000) .build(); + } else { throw new IllegalArgumentException("type not supported:" + resultStr); } } - private Response jsonSuccessResponse(Object result, float took) { + private Response jsonSuccessResponse(String keyName, Object result, float took) { ObjectNode json = JsonNodeFactory.instance.objectNode(); - json.putPOJO("polygons", result); + json.putPOJO(keyName, result); // If you replace GraphHopper with your own brand name, this is fine. // Still it would be highly appreciated if you mention us in your about page! final ObjectNode info = json.putObject("info");