From cf6a9801bd4783213d83bebc27addc59fe634f75 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 25 Oct 2019 18:10:38 +0200 Subject: [PATCH] introduce new POST /route possibility (#1762) * introduce new POST /route endpoint, fixes #1689 * curbSide -> curbside * gzip larger request bodies for POST /route and /matrix * fix key access for GraphHopperWeb and POST /route * rename GHRequest.put into putHint * fix snap_prevention missing in matrix * updated api-doc a bit * improve docs --- .../main/java/com/graphhopper/GHRequest.java | 30 +++- .../api/GHMatrixAbstractRequester.java | 33 ++-- .../api/GHMatrixBatchRequester.java | 66 ++++++-- .../api/GHMatrixSyncRequester.java | 24 +-- .../graphhopper/api/GraphHopperMatrixWeb.java | 11 +- .../com/graphhopper/api/GraphHopperWeb.java | 151 ++++++++++++++---- .../api/GzipRequestInterceptor.java | 87 ++++++++++ ...GraphHopperMatrixWebIntegrationTester.java | 11 ++ .../com/graphhopper/api/GHMatrixSyncTest.java | 5 - .../com/graphhopper/api/GraphHopperWebIT.java | 33 +++- docs/web/api-doc.md | 15 +- tools/pom.xml | 7 +- .../jackson/GHPointDeserializer.java | 2 +- .../graphhopper/jackson/GHRequestMixIn.java | 29 ++++ .../jackson/GraphHopperModule.java | 3 + .../graphhopper/http/GraphHopperBundle.java | 4 +- .../graphhopper/resources/RouteResource.java | 50 +++++- .../http/resources/RouteResourceTest.java | 34 ++++ 18 files changed, 496 insertions(+), 99 deletions(-) create mode 100644 client-hc/src/main/java/com/graphhopper/api/GzipRequestInterceptor.java create mode 100644 web-api/src/main/java/com/graphhopper/jackson/GHRequestMixIn.java diff --git a/api/src/main/java/com/graphhopper/GHRequest.java b/api/src/main/java/com/graphhopper/GHRequest.java index 4d66dcfdcdc..092c53026d4 100644 --- a/api/src/main/java/com/graphhopper/GHRequest.java +++ b/api/src/main/java/com/graphhopper/GHRequest.java @@ -33,11 +33,11 @@ * @author ratrun */ public class GHRequest { - private final List points; + private List points; private final HintsMap hints = new HintsMap(); // List of favored start (1st element) and arrival heading (all other). // Headings are north based azimuth (clockwise) in (0, 360) or NaN for equal preference - private final List favoredHeadings; + private List favoredHeadings; private List pointHints = new ArrayList<>(); private List curbsides = new ArrayList<>(); private List snapPreventions = new ArrayList<>(); @@ -101,7 +101,6 @@ public GHRequest(GHPoint startPlace, GHPoint endPlace) { /** * Set routing request - *

* * @param points List of stopover points in order: start, 1st stop, 2nd stop, ..., end * @param favoredHeadings List of favored headings for starting (start point) and arrival (via @@ -121,7 +120,6 @@ public GHRequest(List points, List favoredHeadings) { /** * Set routing request - *

* * @param points List of stopover points in order: start, 1st stop, 2nd stop, ..., end */ @@ -131,7 +129,6 @@ public GHRequest(List points) { /** * Add stopover point to routing request. - *

* * @param point geographical position (see GHPoint) * @param favoredHeading north based azimuth (clockwise) in (0, 360) or NaN for equal preference @@ -152,7 +149,6 @@ public GHRequest addPoint(GHPoint point, double favoredHeading) { /** * Add stopover point to routing request. - *

* * @param point geographical position (see GHPoint) */ @@ -161,10 +157,22 @@ public GHRequest addPoint(GHPoint point) { return this; } + public void setPoints(List points) { + this.points = points; + if (favoredHeadings.isEmpty()) + this.favoredHeadings = Collections.nCopies(points.size(), Double.NaN); + } + + public void setHeadings(List favoredHeadings) { + this.favoredHeadings = favoredHeadings; + } + /** * @return north based azimuth (clockwise) in (0, 360) or NaN for equal preference */ public double getFavoredHeading(int i) { + if (favoredHeadings.size() != points.size()) + throw new IllegalStateException("Wrong size of headings " + favoredHeadings.size() + " vs. point count " + points.size()); return favoredHeadings.get(i); } @@ -243,6 +251,16 @@ public HintsMap getHints() { return hints; } + /** + * This method sets a key value pair in the hints and is equivalent to getHints().put(String, String) but unrelated + * to the setPointHints method. It is mainly used for deserialization with Jackson. + * + * @see #setPointHints(List) + */ + public void putHint(String fieldName, Object value) { + this.hints.put(fieldName, value); + } + public GHRequest setPointHints(List pointHints) { this.pointHints = pointHints; return this; diff --git a/client-hc/src/main/java/com/graphhopper/api/GHMatrixAbstractRequester.java b/client-hc/src/main/java/com/graphhopper/api/GHMatrixAbstractRequester.java index d87e5aeab8a..e12f2641081 100644 --- a/client-hc/src/main/java/com/graphhopper/api/GHMatrixAbstractRequester.java +++ b/client-hc/src/main/java/com/graphhopper/api/GHMatrixAbstractRequester.java @@ -1,3 +1,20 @@ +/* + * Licensed to GraphHopper GmbH under one or more contributor + * license agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * GraphHopper GmbH licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.graphhopper.api; import com.fasterxml.jackson.databind.JsonNode; @@ -20,13 +37,14 @@ */ public abstract class GHMatrixAbstractRequester { + static final String MATRIX_URL = "https://graphhopper.com/api/1/matrix"; protected final ObjectMapper objectMapper; - protected final Set ignoreSet = new HashSet<>(10); + protected final Set ignoreSet = new HashSet<>(); protected final String serviceUrl; private OkHttpClient downloader; public GHMatrixAbstractRequester() { - this("https://graphhopper.com/api/1/matrix"); + this(MATRIX_URL); } public GHMatrixAbstractRequester(String serviceUrl) { @@ -69,17 +87,6 @@ protected String getJson(String url) throws IOException { } } - protected String postJson(String url, JsonNode data) throws IOException { - Request okRequest = new Request.Builder().url(url).post(RequestBody.create(MT_JSON, data.toString())).build(); - ResponseBody body = null; - try { - body = downloader.newCall(okRequest).execute().body(); - return body.string(); - } finally { - Helper.close(body); - } - } - protected JsonNode toJSON(String url, String str) { try { return objectMapper.readTree(str); diff --git a/client-hc/src/main/java/com/graphhopper/api/GHMatrixBatchRequester.java b/client-hc/src/main/java/com/graphhopper/api/GHMatrixBatchRequester.java index abb61da7c5a..0c491d1baac 100644 --- a/client-hc/src/main/java/com/graphhopper/api/GHMatrixBatchRequester.java +++ b/client-hc/src/main/java/com/graphhopper/api/GHMatrixBatchRequester.java @@ -1,12 +1,32 @@ +/* + * Licensed to GraphHopper GmbH under one or more contributor + * license agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * GraphHopper GmbH licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.graphhopper.api; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.graphhopper.jackson.PathWrapperDeserializer; +import com.graphhopper.util.Helper; import com.graphhopper.util.shapes.GHPoint; import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,22 +35,29 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.graphhopper.api.GraphHopperMatrixWeb.MT_JSON; /** * @author Peter Karich */ public class GHMatrixBatchRequester extends GHMatrixAbstractRequester { - final JsonNodeFactory factory = JsonNodeFactory.instance; private final Logger logger = LoggerFactory.getLogger(getClass()); private int maxIterations = 100; private long sleepAfterGET = 1000; + int unzippedLength = 1000; public GHMatrixBatchRequester() { - super(); + this(MATRIX_URL); } public GHMatrixBatchRequester(String serviceUrl) { - super(serviceUrl); + this(serviceUrl, new OkHttpClient.Builder(). + connectTimeout(5, TimeUnit.SECONDS). + readTimeout(5, TimeUnit.SECONDS). + addInterceptor(new GzipRequestInterceptor()). // gzip the request + build()); } public GHMatrixBatchRequester(String serviceUrl, OkHttpClient client) { @@ -55,14 +82,14 @@ public GHMatrixBatchRequester setSleepAfterGET(long sleepAfterGETMillis) { @Override public MatrixResponse route(GHMRequest ghRequest) { - ObjectNode requestJson = factory.objectNode(); + ObjectNode requestJson = objectMapper.createObjectNode(); List outArraysList = new ArrayList<>(ghRequest.getOutArrays()); if (outArraysList.isEmpty()) { outArraysList.add("weights"); } - ArrayNode outArrayListJson = factory.arrayNode(); + ArrayNode outArrayListJson = objectMapper.createArrayNode(); for (String str : outArraysList) { outArrayListJson.add(str); } @@ -83,6 +110,7 @@ public MatrixResponse route(GHMRequest ghRequest) { requestJson.putArray("to_curbsides").addAll(createStringList(ghRequest.getToPointHints())); } + requestJson.putArray("snap_preventions").addAll(createStringList(ghRequest.getSnapPreventions())); requestJson.putArray("out_arrays").addAll(outArrayListJson); requestJson.put("vehicle", ghRequest.getVehicle()); requestJson.put("elevation", hasElevation); @@ -178,18 +206,34 @@ public MatrixResponse route(GHMRequest ghRequest) { return matrixResponse; } - private final ArrayNode createStringList(List list) { - ArrayNode outList = factory.arrayNode(); + protected String postJson(String url, JsonNode data) throws IOException { + String stringData = data.toString(); + Request.Builder builder = new Request.Builder().url(url).post(RequestBody.create(MT_JSON, stringData)); + // force avoiding our GzipRequestInterceptor for smaller requests ~30 locations + if (stringData.length() < unzippedLength) + builder.header("Content-Encoding", "identity"); + Request okRequest = builder.build(); + ResponseBody body = null; + try { + body = getDownloader().newCall(okRequest).execute().body(); + return body.string(); + } finally { + Helper.close(body); + } + } + + private ArrayNode createStringList(List list) { + ArrayNode outList = objectMapper.createArrayNode(); for (String str : list) { outList.add(str); } return outList; } - protected final ArrayNode createPointList(List list) { - ArrayNode outList = factory.arrayNode(); + private ArrayNode createPointList(List list) { + ArrayNode outList = objectMapper.createArrayNode(); for (GHPoint p : list) { - ArrayNode entry = factory.arrayNode(); + ArrayNode entry = objectMapper.createArrayNode(); entry.add(p.lon); entry.add(p.lat); outList.add(entry); diff --git a/client-hc/src/main/java/com/graphhopper/api/GHMatrixSyncRequester.java b/client-hc/src/main/java/com/graphhopper/api/GHMatrixSyncRequester.java index 5eadec3c33c..74e6d32490b 100644 --- a/client-hc/src/main/java/com/graphhopper/api/GHMatrixSyncRequester.java +++ b/client-hc/src/main/java/com/graphhopper/api/GHMatrixSyncRequester.java @@ -9,7 +9,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.TimeUnit; /** * @author Peter Karich @@ -21,15 +20,13 @@ public GHMatrixSyncRequester() { initIgnore(); } - public GHMatrixSyncRequester(String serviceUrl, OkHttpClient client) { - super(serviceUrl, client); + public GHMatrixSyncRequester(String serviceUrl) { + super(serviceUrl); initIgnore(); } - public GHMatrixSyncRequester(String serviceUrl) { - super(serviceUrl, new OkHttpClient.Builder(). - connectTimeout(15, TimeUnit.SECONDS). - readTimeout(15, TimeUnit.SECONDS).build()); + public GHMatrixSyncRequester(String serviceUrl, OkHttpClient client) { + super(serviceUrl, client); initIgnore(); } @@ -70,15 +67,22 @@ public MatrixResponse route(GHMRequest ghRequest) { } for (String type : outArraysList) { - if (!type.isEmpty()) { + if (!type.isEmpty()) outArrayStr += "&"; - } outArrayStr += "out_array=" + type; } + String snapPreventionStr = ""; + for (String snapPrevention : ghRequest.getSnapPreventions()) { + if (!snapPreventionStr.isEmpty()) + snapPreventionStr += "&"; + + snapPreventionStr += "snap_prevention=" + snapPrevention; + } + String url = buildURL("", ghRequest); - url += "&" + pointsStr + "&" + pointHintsStr + "&" + curbsidesStr + "&" + outArrayStr; + url += "&" + pointsStr + "&" + pointHintsStr + "&" + curbsidesStr + "&" + outArrayStr + "&" + snapPreventionStr; if (!Helper.isEmpty(ghRequest.getVehicle())) { url += "&vehicle=" + ghRequest.getVehicle(); } diff --git a/client-hc/src/main/java/com/graphhopper/api/GraphHopperMatrixWeb.java b/client-hc/src/main/java/com/graphhopper/api/GraphHopperMatrixWeb.java index 85117966e2d..e828187bf14 100644 --- a/client-hc/src/main/java/com/graphhopper/api/GraphHopperMatrixWeb.java +++ b/client-hc/src/main/java/com/graphhopper/api/GraphHopperMatrixWeb.java @@ -4,14 +4,13 @@ import okhttp3.MediaType; /** - * * @author Peter Karich */ public class GraphHopperMatrixWeb { - public static final String SERVICE_URL = "service_url"; + static final String SERVICE_URL = "service_url"; public static final String KEY = "key"; - public static final MediaType MT_JSON = MediaType.parse("application/json; charset=utf-8"); + static final MediaType MT_JSON = MediaType.parse("application/json; charset=utf-8"); private final GHMatrixAbstractRequester requester; private String key; @@ -37,10 +36,10 @@ public GraphHopperMatrixWeb setKey(String key) { } public MatrixResponse route(GHMRequest request) { - if (!Helper.isEmpty(key)) { + if (!Helper.isEmpty(key)) request.getHints().put(KEY, key); - } - + if (!request.getPathDetails().isEmpty()) + throw new IllegalArgumentException("Path details are not supported for the Matrix API"); request.compactPointHints(); return requester.route(request); } diff --git a/client-hc/src/main/java/com/graphhopper/api/GraphHopperWeb.java b/client-hc/src/main/java/com/graphhopper/api/GraphHopperWeb.java index 8f1b2344941..721a68285eb 100644 --- a/client-hc/src/main/java/com/graphhopper/api/GraphHopperWeb.java +++ b/client-hc/src/main/java/com/graphhopper/api/GraphHopperWeb.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.graphhopper.GHRequest; import com.graphhopper.GHResponse; import com.graphhopper.GraphHopperAPI; @@ -31,16 +33,21 @@ import com.graphhopper.util.shapes.GHPoint; import okhttp3.OkHttpClient; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.ResponseBody; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import static com.graphhopper.api.GraphHopperMatrixWeb.*; import static com.graphhopper.util.Helper.round6; import static com.graphhopper.util.Helper.toLowerCase; import static com.graphhopper.util.Parameters.Curbsides.CURBSIDE_ANY; +import static com.graphhopper.util.Parameters.Routing.CALC_POINTS; +import static com.graphhopper.util.Parameters.Routing.INSTRUCTIONS; /** * Main wrapper of the GraphHopper Directions API for a simple and efficient @@ -58,7 +65,10 @@ public class GraphHopperWeb implements GraphHopperAPI { private boolean calcPoints = true; private boolean elevation = false; private String optimize = "false"; + private boolean postRequest = true; + int unzippedLength = 1000; private final Set ignoreSet; + private final Set ignoreSetForPost; public static final String TIMEOUT = "timeout"; private final long DEFAULT_TIMEOUT = 5000; @@ -72,15 +82,24 @@ public GraphHopperWeb(String serviceUrl) { downloader = new OkHttpClient.Builder(). connectTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS). readTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS). + addInterceptor(new GzipRequestInterceptor()). build(); - // some parameters are supported directly via Java API so ignore them when writing the getHints map + ignoreSetForPost = new HashSet<>(); + ignoreSetForPost.add(KEY); + ignoreSetForPost.add(SERVICE_URL); + ignoreSetForPost.add(CALC_POINTS); + ignoreSetForPost.add(INSTRUCTIONS); + ignoreSetForPost.add("elevation"); + ignoreSetForPost.add("optimize"); + ignoreSetForPost.add("points_encoded"); + ignoreSet = new HashSet<>(); - ignoreSet.add("calc_points"); + ignoreSet.add(KEY); + ignoreSet.add(CALC_POINTS); ignoreSet.add("calcpoints"); - ignoreSet.add("instructions"); + ignoreSet.add(INSTRUCTIONS); ignoreSet.add("elevation"); - ignoreSet.add("key"); ignoreSet.add("optimize"); // some parameters are in the request: @@ -120,6 +139,14 @@ public GraphHopperWeb setKey(String key) { return this; } + /** + * Use new endpoint 'POST /route' instead of 'GET /route' + */ + public GraphHopperWeb setPostRequest(boolean postRequest) { + this.postRequest = postRequest; + return this; + } + /** * Enable or disable calculating points for the way. The default is true. */ @@ -159,13 +186,16 @@ public GraphHopperWeb setOptimize(String optimize) { return this; } - @Override - public GHResponse route(GHRequest request) { + public GHResponse route(GHRequest ghRequest) { ResponseBody rspBody = null; try { - Request okRequest = createRequest(request); - rspBody = getClientForRequest(request).newCall(okRequest).execute().body(); + boolean tmpElevation = ghRequest.getHints().getBool("elevation", elevation); + boolean tmpTurnDescription = ghRequest.getHints().getBool("turn_description", true); + ghRequest.getHints().remove("turn_description"); // do not include in request + + Request okRequest = postRequest ? createPostRequest(ghRequest) : createGetRequest(ghRequest); + rspBody = getClientForRequest(ghRequest).newCall(okRequest).execute().body(); JsonNode json = objectMapper.reader().readTree(rspBody.byteStream()); GHResponse res = new GHResponse(); @@ -175,9 +205,6 @@ public GHResponse route(GHRequest request) { JsonNode paths = json.get("paths"); - boolean tmpElevation = request.getHints().getBool("elevation", elevation); - boolean tmpTurnDescription = request.getHints().getBool("turn_description", true); - for (JsonNode path : paths) { PathWrapper altRsp = PathWrapperDeserializer.createPathWrapper(objectMapper, path, tmpElevation, tmpTurnDescription); res.add(altRsp); @@ -186,7 +213,7 @@ public GHResponse route(GHRequest request) { return res; } catch (Exception ex) { - throw new RuntimeException("Problem while fetching path " + request.getPoints() + ": " + ex.getMessage(), ex); + throw new RuntimeException("Problem while fetching path " + ghRequest.getPoints() + ": " + ex.getMessage(), ex); } finally { Helper.close(rspBody); } @@ -205,24 +232,67 @@ private OkHttpClient getClientForRequest(GHRequest request) { return client; } - private Request createRequest(GHRequest request) { - boolean tmpInstructions = request.getHints().getBool("instructions", instructions); - boolean tmpCalcPoints = request.getHints().getBool("calc_points", calcPoints); - String tmpOptimize = request.getHints().get("optimize", optimize); + private Request createPostRequest(GHRequest ghRequest) { + String tmpServiceURL = ghRequest.getHints().get(SERVICE_URL, routeServiceUrl); + String url = tmpServiceURL + "?"; + if (!Helper.isEmpty(key)) + url += "key=" + key; + + ObjectNode requestJson = objectMapper.createObjectNode(); + requestJson.putArray("points").addAll(createPointList(ghRequest.getPoints())); + if (!ghRequest.getPointHints().isEmpty()) + requestJson.putArray("point_hints").addAll(createStringList(ghRequest.getPointHints())); + if (!ghRequest.getCurbsides().isEmpty()) + requestJson.putArray("curbsides").addAll(createStringList(ghRequest.getCurbsides())); + if (!ghRequest.getSnapPreventions().isEmpty()) + requestJson.putArray("snap_preventions").addAll(createStringList(ghRequest.getSnapPreventions())); + if (!ghRequest.getPathDetails().isEmpty()) + requestJson.putArray("details").addAll(createStringList(ghRequest.getPathDetails())); + + requestJson.put("locale", ghRequest.getLocale().toString()); + if (!ghRequest.getAlgorithm().isEmpty()) + requestJson.put("algorithm", ghRequest.getAlgorithm()); + + requestJson.put("points_encoded", true); + requestJson.put(INSTRUCTIONS, ghRequest.getHints().getBool(INSTRUCTIONS, instructions)); + requestJson.put(CALC_POINTS, ghRequest.getHints().getBool(CALC_POINTS, calcPoints)); + requestJson.put("elevation", ghRequest.getHints().getBool("elevation", elevation)); + requestJson.put("optimize", ghRequest.getHints().get("optimize", optimize)); + + Map hintsMap = ghRequest.getHints().toMap(); + for (String hintKey : hintsMap.keySet()) { + if (ignoreSetForPost.contains(hintKey)) + continue; + + String hint = hintsMap.get(hintKey); + requestJson.put(hintKey, hint); + } + String stringData = requestJson.toString(); + Request.Builder builder = new Request.Builder().url(url).post(RequestBody.create(MT_JSON, stringData)); + // force avoiding our GzipRequestInterceptor for smaller requests ~30 locations + if (stringData.length() < unzippedLength) + builder.header("Content-Encoding", "identity"); + return builder.build(); + } + + private Request createGetRequest(GHRequest ghRequest) { + boolean tmpInstructions = ghRequest.getHints().getBool(INSTRUCTIONS, instructions); + boolean tmpCalcPoints = ghRequest.getHints().getBool(CALC_POINTS, calcPoints); + String tmpOptimize = ghRequest.getHints().get("optimize", optimize); if (tmpInstructions && !tmpCalcPoints) { throw new IllegalStateException("Cannot calculate instructions without points (only points without instructions). " + "Use calc_points=false and instructions=false to disable point and instruction calculation"); } - boolean tmpElevation = request.getHints().getBool("elevation", elevation); + boolean tmpElevation = ghRequest.getHints().getBool("elevation", elevation); String places = ""; - for (GHPoint p : request.getPoints()) { + for (GHPoint p : ghRequest.getPoints()) { places += "point=" + round6(p.lat) + "," + round6(p.lon) + "&"; } - String type = request.getHints().get("type", "json"); + String type = ghRequest.getHints().get("type", "json"); String url = routeServiceUrl + "?" @@ -231,23 +301,23 @@ private Request createRequest(GHRequest request) { + "&instructions=" + tmpInstructions + "&points_encoded=true" + "&calc_points=" + tmpCalcPoints - + "&algorithm=" + request.getAlgorithm() - + "&locale=" + request.getLocale().toString() + + "&algorithm=" + ghRequest.getAlgorithm() + + "&locale=" + ghRequest.getLocale().toString() + "&elevation=" + tmpElevation + "&optimize=" + tmpOptimize; - if (!request.getVehicle().isEmpty()) { - url += "&vehicle=" + request.getVehicle(); + if (!ghRequest.getVehicle().isEmpty()) { + url += "&vehicle=" + ghRequest.getVehicle(); } - for (String details : request.getPathDetails()) { + for (String details : ghRequest.getPathDetails()) { url += "&" + Parameters.Details.PATH_DETAILS + "=" + details; } // append *all* point hints only if at least *one* is not empty - for (String checkEmptyHint : request.getPointHints()) { + for (String checkEmptyHint : ghRequest.getPointHints()) { if (!checkEmptyHint.isEmpty()) { - for (String hint : request.getPointHints()) { + for (String hint : ghRequest.getPointHints()) { url += "&" + Parameters.Routing.POINT_HINT + "=" + WebHelper.encodeURL(hint); } break; @@ -255,16 +325,16 @@ private Request createRequest(GHRequest request) { } // append *all* curbsides only if at least *one* is not CURBSIDE_ANY - for (String checkEitherSide : request.getCurbsides()) { + for (String checkEitherSide : ghRequest.getCurbsides()) { if (!checkEitherSide.equals(CURBSIDE_ANY)) { - for (String curbside : request.getCurbsides()) { + for (String curbside : ghRequest.getCurbsides()) { url += "&" + Parameters.Routing.CURBSIDE + "=" + WebHelper.encodeURL(curbside); } break; } } - for (String snapPrevention : request.getSnapPreventions()) { + for (String snapPrevention : ghRequest.getSnapPreventions()) { url += "&" + Parameters.Routing.SNAP_PREVENTION + "=" + WebHelper.encodeURL(snapPrevention); } @@ -272,7 +342,7 @@ private Request createRequest(GHRequest request) { url += "&key=" + WebHelper.encodeURL(key); } - for (Map.Entry entry : request.getHints().toMap().entrySet()) { + for (Map.Entry entry : ghRequest.getHints().toMap().entrySet()) { String urlKey = entry.getKey(); String urlValue = entry.getValue(); @@ -292,7 +362,7 @@ private Request createRequest(GHRequest request) { public String export(GHRequest ghRequest) { String str = "Creating request failed"; try { - Request okRequest = createRequest(ghRequest); + Request okRequest = createGetRequest(ghRequest); str = getClientForRequest(ghRequest).newCall(okRequest).execute().body().string(); return str; @@ -301,4 +371,23 @@ public String export(GHRequest ghRequest) { + ", error: " + ex.getMessage() + " response: " + str, ex); } } + + private ArrayNode createStringList(List list) { + ArrayNode outList = objectMapper.createArrayNode(); + for (String str : list) { + outList.add(str); + } + return outList; + } + + private ArrayNode createPointList(List list) { + ArrayNode outList = objectMapper.createArrayNode(); + for (GHPoint p : list) { + ArrayNode entry = objectMapper.createArrayNode(); + entry.add(p.lon); + entry.add(p.lat); + outList.add(entry); + } + return outList; + } } diff --git a/client-hc/src/main/java/com/graphhopper/api/GzipRequestInterceptor.java b/client-hc/src/main/java/com/graphhopper/api/GzipRequestInterceptor.java new file mode 100644 index 00000000000..afe559c9736 --- /dev/null +++ b/client-hc/src/main/java/com/graphhopper/api/GzipRequestInterceptor.java @@ -0,0 +1,87 @@ +/* + * Licensed to GraphHopper GmbH under one or more contributor + * license agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * GraphHopper GmbH licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.graphhopper.api; + +import okhttp3.*; +import okio.Buffer; +import okio.BufferedSink; +import okio.GzipSink; +import okio.Okio; + +import java.io.IOException; + +/** + * Encodes request bodies using gzip. Taken from https://github.com/square/okhttp/issues/350 + */ +class GzipRequestInterceptor implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + Request originalRequest = chain.request(); + if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) { + return chain.proceed(originalRequest); + } + + Request compressedRequest = originalRequest.newBuilder() + .header("Content-Encoding", "gzip") + .method(originalRequest.method(), forceContentLength(gzip(originalRequest.body()))) + .build(); + return chain.proceed(compressedRequest); + } + + private RequestBody forceContentLength(final RequestBody requestBody) throws IOException { + final Buffer buffer = new Buffer(); + requestBody.writeTo(buffer); + return new RequestBody() { + @Override + public MediaType contentType() { + return requestBody.contentType(); + } + + @Override + public long contentLength() { + return buffer.size(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + sink.write(buffer.snapshot()); + } + }; + } + + private RequestBody gzip(final RequestBody body) { + return new RequestBody() { + @Override + public MediaType contentType() { + return body.contentType(); + } + + @Override + public long contentLength() { + return -1; // We don't know the compressed length in advance! + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + BufferedSink gzipSink = Okio.buffer(new GzipSink(sink)); + body.writeTo(gzipSink); + gzipSink.close(); + } + }; + } +} \ No newline at end of file diff --git a/client-hc/src/test/java/com/graphhopper/api/AbstractGraphHopperMatrixWebIntegrationTester.java b/client-hc/src/test/java/com/graphhopper/api/AbstractGraphHopperMatrixWebIntegrationTester.java index e019b54f16a..3359f7b0d4a 100644 --- a/client-hc/src/test/java/com/graphhopper/api/AbstractGraphHopperMatrixWebIntegrationTester.java +++ b/client-hc/src/test/java/com/graphhopper/api/AbstractGraphHopperMatrixWebIntegrationTester.java @@ -104,6 +104,17 @@ public void testPOSTMatrixQueryWithPointHints() { assertEquals("Array length of point_hints must match length of points (or from/to equivalent)", res.getErrors().get(0).getMessage()); } + @Test + public void testMatrixSnapPrevention() { + GHMRequest req = new GHMRequest(); + req.addPoint(new GHPoint(52.480271, 13.418941)); + req.addPoint(new GHPoint(52.462834, 13.438854)); + req.addOutArray("distances"); + req.setSnapPreventions(Arrays.asList("motorway")); + MatrixResponse res = ghMatrix.route(req); + assertEquals(2860, res.getDistance(0, 1), 30); + } + @Test public void testConnectionNotFound() { GHMRequest req = new GHMRequest(); diff --git a/client-hc/src/test/java/com/graphhopper/api/GHMatrixSyncTest.java b/client-hc/src/test/java/com/graphhopper/api/GHMatrixSyncTest.java index 936013a4a7c..a59a5002f07 100644 --- a/client-hc/src/test/java/com/graphhopper/api/GHMatrixSyncTest.java +++ b/client-hc/src/test/java/com/graphhopper/api/GHMatrixSyncTest.java @@ -21,11 +21,6 @@ GraphHopperMatrixWeb createMatrixClient(String jsonStr) throws IOException { final String finalJsonStr = json.toString(); return new GraphHopperMatrixWeb(new GHMatrixSyncRequester("") { - @Override - protected String postJson(String url, JsonNode data) throws IOException { - return "{\"job_id\": \"1\"}"; - } - @Override protected String getJson(String url) throws IOException { return finalJsonStr; diff --git a/client-hc/src/test/java/com/graphhopper/api/GraphHopperWebIT.java b/client-hc/src/test/java/com/graphhopper/api/GraphHopperWebIT.java index c7cb03dc51b..c94c9046e3c 100644 --- a/client-hc/src/test/java/com/graphhopper/api/GraphHopperWebIT.java +++ b/client-hc/src/test/java/com/graphhopper/api/GraphHopperWebIT.java @@ -14,12 +14,14 @@ import com.graphhopper.util.exceptions.PointNotFoundException; import com.graphhopper.util.exceptions.PointOutOfBoundsException; import com.graphhopper.util.shapes.GHPoint; -import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import java.io.IOException; import java.net.SocketTimeoutException; import java.util.Arrays; +import java.util.Collection; import java.util.List; import static org.junit.Assert.*; @@ -27,17 +29,32 @@ /** * @author Peter Karich */ +@RunWith(Parameterized.class) public class GraphHopperWebIT { - public static final String KEY = System.getProperty("key", "78da6e9a-273e-43d1-bdda-8f24e007a1fa"); + static final String KEY = System.getProperty("key", "78da6e9a-273e-43d1-bdda-8f24e007a1fa"); + private final GraphHopperWeb gh; + private final GraphHopperMatrixWeb ghMatrix; - private final GraphHopperWeb gh = new GraphHopperWeb(); - private final GraphHopperMatrixWeb ghMatrix = new GraphHopperMatrixWeb(); + public GraphHopperWebIT(boolean postRequest, int unzippedLength) { + gh = new GraphHopperWeb().setPostRequest(postRequest). + setKey(KEY); + gh.unzippedLength = unzippedLength; - @Before - public void setUp() { - gh.setKey(KEY); - ghMatrix.setKey(KEY); + GHMatrixBatchRequester requester = new GHMatrixBatchRequester(); + requester.unzippedLength = unzippedLength; + ghMatrix = new GraphHopperMatrixWeb(requester). + setKey(KEY); + } + + @Parameterized.Parameters(name = "POST = {0}, unzippedLength = {1}") + public static Collection configs() { + return Arrays.asList(new Object[][]{ + {false, -1}, + // TODO later: test post request against API +// {true, 1000}, +// {true, 0} + }); } @Test diff --git a/docs/web/api-doc.md b/docs/web/api-doc.md index 2cd4c7959a3..81b3f7edd20 100644 --- a/docs/web/api-doc.md +++ b/docs/web/api-doc.md @@ -8,7 +8,17 @@ server you need to understand how to use it. There is a separate [JavaScript](ht The URL path of the local instance is [http://localhost:8989](http://localhost:8989) -The endpoint to obtain the route is `/route` +The endpoint to obtain the route is `/route` via GET. + +## HTTP POST + +The GET request has an URL length limitation, so it won't work for many locations per request. In those cases use a HTTP POST request with JSON data as input. The POST request is identical except that all singular parameter names are named as their plural for a POST request. All effected parameters are: `points`, `snap_preventions`, `curbsides` and `point_hints`. (`details` stays `details`) + +Please note that unlike to the GET endpoint, points are specified in `[longitude, latitude]` order. For example `point=10,11&point=20,22` will be the following JSON: + +```json +{ "points": [[11,10], [22,20]] } +``` ## Parameters @@ -28,6 +38,9 @@ type | json | Specifies the resulting format of the route, for `j point_hint | - | Optional parameter. Specifies a hint for each `point` parameter to prefer a certain street for the closest location lookup. E.g. if there is an address or house with two or more neighboring streets you can control for which street the closest location is looked up. snap_prevention | - | Optional parameter to avoid snapping to a certain road class or road environment. Current supported values: `motorway`, `trunk`, `ferry`, `tunnel`, `bridge` and `ford`. Multiple values are specified like `snap_prevention=ferry&snap_prevention=motorway` details | - | Optional parameter. You can request additional details for the route: `average_speed`, `street_name`, `edge_id`, `road_class`, `road_environment`, `max_speed` and `time` (and see which other values are configured in `graph.encoded_values`). Multiple values are specified like `details=average_speed&details=time`. The returned format for one detail segment is `[fromRef, toRef, value]`. The `ref` references the points of the response. Value can also be `null` if the property does not exist for one detail segment. +edge_base | false | Internal parameter to force edge-based algorithm. +curbside | any | Optional parameter applicable to edge-based routing only. It specifies on which side a query point should be relative to the driver when she leaves/arrives at a start/target/via point. Possible values: right, left, any. Specify for every point parameter. See similar heading parameter. +force_curbside | false | True if the curbside parameters should lead to an exception if they cannot be fulfilled. ### GPX diff --git a/tools/pom.xml b/tools/pom.xml index 5a778e0123b..ddf4beb296f 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -41,12 +41,11 @@ jackson-databind ${jackson.version} - org.slf4j slf4j-api ${slf4j.version} - + org.slf4j slf4j-log4j12 @@ -71,14 +70,14 @@ org.apache.maven.plugins - maven-assembly-plugin + maven-assembly-plugin com.graphhopper.tools.Measurement - + jar-with-dependencies diff --git a/web-api/src/main/java/com/graphhopper/jackson/GHPointDeserializer.java b/web-api/src/main/java/com/graphhopper/jackson/GHPointDeserializer.java index ce3f49fff17..212774f8db2 100644 --- a/web-api/src/main/java/com/graphhopper/jackson/GHPointDeserializer.java +++ b/web-api/src/main/java/com/graphhopper/jackson/GHPointDeserializer.java @@ -10,7 +10,7 @@ class GHPointDeserializer extends JsonDeserializer { @Override - public GHPoint deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + public GHPoint deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { double[] bounds = jsonParser.readValueAs(double[].class); return GHPoint.fromJson(bounds); } diff --git a/web-api/src/main/java/com/graphhopper/jackson/GHRequestMixIn.java b/web-api/src/main/java/com/graphhopper/jackson/GHRequestMixIn.java new file mode 100644 index 00000000000..e5c706ae79a --- /dev/null +++ b/web-api/src/main/java/com/graphhopper/jackson/GHRequestMixIn.java @@ -0,0 +1,29 @@ +package com.graphhopper.jackson; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.graphhopper.GHRequest; + +import java.util.List; + +/** + * With this approach we avoid the jackson annotations dependency in core + */ +interface GHRequestMixIn { + + // a good trick to serialize unknown properties into the HintsMap + @JsonAnySetter + void putHint(String fieldName, Object value); + + @JsonProperty("point_hints") + GHRequest setPointHints(List pointHints); + + @JsonProperty("snap_preventions") + GHRequest setSnapPreventions(List snapPreventions); + + @JsonProperty("details") + GHRequest setPathDetails(List pathDetails); + + @JsonProperty("curbsides") + GHRequest setCurbsides(List curbsides); +} diff --git a/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java b/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java index 3c5ef5049d9..41f41c9dae1 100644 --- a/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java +++ b/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java @@ -1,9 +1,11 @@ package com.graphhopper.jackson; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.graphhopper.GHRequest; import com.graphhopper.GHResponse; import com.graphhopper.MultiException; import com.graphhopper.PathWrapper; +import com.graphhopper.routing.util.HintsMap; import com.graphhopper.util.CmdArgs; import com.graphhopper.util.InstructionList; import com.graphhopper.util.details.PathDetail; @@ -13,6 +15,7 @@ public class GraphHopperModule extends SimpleModule { public GraphHopperModule() { + setMixInAnnotation(GHRequest.class, GHRequestMixIn.class); addDeserializer(GHResponse.class, new GHResponseDeserializer()); addDeserializer(PathWrapper.class, new PathWrapperDeserializer()); addDeserializer(BBox.class, new BBoxDeserializer()); diff --git a/web-bundle/src/main/java/com/graphhopper/http/GraphHopperBundle.java b/web-bundle/src/main/java/com/graphhopper/http/GraphHopperBundle.java index df017b1eb13..7673666b962 100644 --- a/web-bundle/src/main/java/com/graphhopper/http/GraphHopperBundle.java +++ b/web-bundle/src/main/java/com/graphhopper/http/GraphHopperBundle.java @@ -232,11 +232,11 @@ protected void configure() { }); environment.lifecycle().manage(new Managed() { @Override - public void start() throws Exception { + public void start() { } @Override - public void stop() throws Exception { + public void stop() { locationIndex.close(); gtfsStorage.close(); graphHopperStorage.close(); diff --git a/web-bundle/src/main/java/com/graphhopper/resources/RouteResource.java b/web-bundle/src/main/java/com/graphhopper/resources/RouteResource.java index cbbefae430f..3d17c90ac14 100644 --- a/web-bundle/src/main/java/com/graphhopper/resources/RouteResource.java +++ b/web-bundle/src/main/java/com/graphhopper/resources/RouteResource.java @@ -142,7 +142,6 @@ public Response doGet( GHResponse ghResponse = graphHopper.route(request); - // TODO: Request logging and timing should perhaps be done somewhere outside float took = sw.stop().getSeconds(); String infoStr = httpReq.getRemoteAddr() + " " + httpReq.getLocale() + " " + httpReq.getHeader("User-Agent"); String logStr = httpReq.getQueryString() + " " + infoStr + " " + requestPoints + ", took:" @@ -169,6 +168,55 @@ public Response doGet( } } + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, "application/gpx+xml"}) + public Response doPost(GHRequest request, @Context HttpServletRequest httpReq) { + if (request == null) + throw new IllegalArgumentException("Empty request"); + + StopWatch sw = new StopWatch().start(); + GHResponse ghResponse = graphHopper.route(request); + + boolean instructions = request.getHints().getBool(INSTRUCTIONS, true); + boolean writeGPX = "gpx".equalsIgnoreCase(request.getHints().get("type", "json")); + instructions = writeGPX || instructions; + boolean enableElevation = request.getHints().getBool("elevation", false); + boolean calcPoints = request.getHints().getBool(CALC_POINTS, true); + boolean pointsEncoded = request.getHints().getBool("points_encoded", true); + + /* default to false for the route part in next API version, see #437 */ + boolean withRoute = request.getHints().getBool("gpx.route", true); + boolean withTrack = request.getHints().getBool("gpx.track", true); + boolean withWayPoints = request.getHints().getBool("gpx.waypoints", false); + String trackName = request.getHints().get("gpx.trackname", "GraphHopper Track"); + String timeString = request.getHints().get("gpx.millis", ""); + float took = sw.stop().getSeconds(); + String infoStr = httpReq.getRemoteAddr() + " " + httpReq.getLocale() + " " + httpReq.getHeader("User-Agent"); + String logStr = httpReq.getQueryString() + " " + infoStr + " " + request.getPoints().size() + ", took:" + + took + ", " + request.getAlgorithm() + ", " + request.getWeighting() + ", " + request.getVehicle(); + + if (ghResponse.hasErrors()) { + logger.error(logStr + ", errors:" + ghResponse.getErrors()); + throw new MultiException(ghResponse.getErrors()); + } else { + logger.info(logStr + ", alternatives: " + ghResponse.getAll().size() + + ", distance0: " + ghResponse.getBest().getDistance() + + ", weight0: " + ghResponse.getBest().getRouteWeight() + + ", time0: " + Math.round(ghResponse.getBest().getTime() / 60000f) + "min" + + ", points0: " + ghResponse.getBest().getPoints().getSize() + + ", debugInfo: " + ghResponse.getDebugInfo()); + return writeGPX ? + gpxSuccessResponseBuilder(ghResponse, timeString, trackName, enableElevation, withRoute, withTrack, withWayPoints, Constants.VERSION). + header("X-GH-Took", "" + Math.round(took * 1000)). + build() + : + Response.ok(WebHelper.jsonObject(ghResponse, instructions, calcPoints, enableElevation, pointsEncoded, took)). + header("X-GH-Took", "" + Math.round(took * 1000)). + build(); + } + } + private void enableEdgeBasedIfThereAreCurbsides(List curbsides, GHRequest request) { if (!curbsides.isEmpty()) { if (!request.getHints().getBool(EDGE_BASED, true)) { diff --git a/web/src/test/java/com/graphhopper/http/resources/RouteResourceTest.java b/web/src/test/java/com/graphhopper/http/resources/RouteResourceTest.java index 023205a334e..1aaec00a0e0 100644 --- a/web/src/test/java/com/graphhopper/http/resources/RouteResourceTest.java +++ b/web/src/test/java/com/graphhopper/http/resources/RouteResourceTest.java @@ -40,6 +40,7 @@ import org.junit.ClassRule; import org.junit.Test; +import javax.ws.rs.client.Entity; import javax.ws.rs.core.Response; import java.io.File; import java.util.Arrays; @@ -91,6 +92,20 @@ public void testBasicQuery() { assertTrue("distance wasn't correct:" + distance, distance < 9500); } + @Test + public void testBasicPostQuery() { + String jsonStr = "{ \"points\": [[1.536198,42.554851], [1.548128, 42.510071]] }"; + final Response response = app.client().target("http://localhost:8080/route").request().post(Entity.json(jsonStr)); + assertEquals(200, response.getStatus()); + JsonNode json = response.readEntity(JsonNode.class); + JsonNode infoJson = json.get("info"); + assertFalse(infoJson.has("errors")); + JsonNode path = json.get("paths").get(0); + double distance = path.get("distance").asDouble(); + assertTrue("distance wasn't correct:" + distance, distance > 9000); + assertTrue("distance wasn't correct:" + distance, distance < 9500); + } + @Test public void testWrongPointFormat() { final Response response = app.client().target("http://localhost:8080/route?point=1234&point=42.510071,1.548128").request().buildGet().invoke(); @@ -288,6 +303,7 @@ public void testInitInstructionsWithTurnDescription() { request.getHints().put("turn_description", false); rsp = hopper.route(request); + assertFalse(rsp.hasErrors()); assertEquals("Carrer Antoni Fiter i Rossell", rsp.getBest().getInstructions().get(3).getName()); } @@ -322,6 +338,24 @@ public void testSnapPreventionsAndPointHints() { assertEquals(490, rsp.getBest().getDistance(), 2); } + @Test + public void testPostWithPointHintsAndSnapPrevention() { + String jsonStr = "{ \"points\": [[1.53285,42.511139], [1.532271,42.508165]], " + + "\"point_hints\":[\"Avinguda Fiter i Rossell\",\"\"] }"; + Response response = app.client().target("http://localhost:8080/route").request().post(Entity.json(jsonStr)); + assertEquals(200, response.getStatus()); + JsonNode path = response.readEntity(JsonNode.class).get("paths").get(0); + assertEquals(1590, path.get("distance").asDouble(), 2); + + jsonStr = "{ \"points\": [[1.53285,42.511139], [1.532271,42.508165]], " + + "\"point_hints\":[\"Tunèl del Pont Pla\",\"\"], " + + "\"snap_preventions\": [\"tunnel\"] }"; + response = app.client().target("http://localhost:8080/route").request().post(Entity.json(jsonStr)); + assertEquals(200, response.getStatus()); + path = response.readEntity(JsonNode.class).get("paths").get(0); + assertEquals(490, path.get("distance").asDouble(), 2); + } + @Test public void testGraphHopperWebRealExceptions() { GraphHopperAPI hopper = new com.graphhopper.api.GraphHopperWeb();