From cabd8b17861cdc1504de41a75e5cd1a4e447e6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Rodrigo?= Date: Tue, 8 Oct 2024 23:06:44 +0200 Subject: [PATCH 1/2] Add web API param to GHMatrixRequest --- .../routing/matrix/GHMatrixRequest.java | 105 +++++++++++++++++- .../routing/matrix/RouterMatrix.java | 12 ++ 2 files changed, 114 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/graphhopper/routing/matrix/GHMatrixRequest.java b/core/src/main/java/com/graphhopper/routing/matrix/GHMatrixRequest.java index dd97297c5d6..8b1e916a215 100644 --- a/core/src/main/java/com/graphhopper/routing/matrix/GHMatrixRequest.java +++ b/core/src/main/java/com/graphhopper/routing/matrix/GHMatrixRequest.java @@ -1,5 +1,8 @@ package com.graphhopper.routing.matrix; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.graphhopper.util.CustomModel; import com.graphhopper.util.Helper; import com.graphhopper.util.PMap; import com.graphhopper.util.Parameters; @@ -17,8 +20,14 @@ public class GHMatrixRequest { private String profile = ""; private final List origins = new ArrayList<>(); private final List destinations = new ArrayList<>(); + private final List originPointHints = new ArrayList<>(); + private final List destinationPointHints = new ArrayList<>(); + private List snapPreventions = new ArrayList<>(); + private List originCurbsides = new ArrayList<>(); + private List destinationCurbsides = new ArrayList<>(); private final PMap hints = new PMap(); private boolean failFast = true; + private CustomModel customModel; /** * One or more locations to use as the starting point for calculating travel distance and time. @@ -27,8 +36,10 @@ public List getOrigins() { return origins; } - public void setOrigins(List origins) { + @JsonProperty("from_points") + public GHMatrixRequest setOrigins(List origins) { this.origins.addAll(origins); + return this; } /** @@ -38,8 +49,16 @@ public List getDestinations() { return destinations; } - public void setDestinations(List destinations) { + @JsonProperty("to_points") + public GHMatrixRequest setDestinations(List destinations) { this.destinations.addAll(destinations); + return this; + } + + public GHMatrixRequest setPoints(List origins) { + this.setOrigins(origins); + this.setDestinations(origins); + return this; } public String getAlgorithm() { @@ -65,6 +84,14 @@ public PMap getHints() { return hints; } + /** + * This method sets a key value pair in the hints and is unrelated to the setPointHints method. + * It is mainly used for deserialization with Jackson. + * + * @see #setPointHints(List) + */ + // a good trick to serialize unknown properties into the HintsMap + @JsonAnySetter public GHMatrixRequest putHint(String fieldName, Object value) { this.hints.putObject(fieldName, value); return this; @@ -74,7 +101,79 @@ public boolean isFailFast() { return failFast; } - public void setFailFast(boolean failFast) { + public GHMatrixRequest setFailFast(boolean failFast) { this.failFast = failFast; + return this; + } + + public List getOriginPointHints() { + return originPointHints; + } + + @JsonProperty("from_point_hints") + public GHMatrixRequest setOriginPointHints(List pointHints) { + this.originPointHints.addAll(pointHints); + return this; + } + + public List getDestinationPointHints() { + return destinationPointHints; + } + + @JsonProperty("to_point_hints") + public GHMatrixRequest setDestinationPointHints(List pointHints) { + this.destinationPointHints.addAll(pointHints); + return this; + } + + @JsonProperty("point_hints") + public GHMatrixRequest setPointHints(List pointHints) { + this.originPointHints.addAll(pointHints); + this.destinationPointHints.addAll(pointHints); + return this; + } + + public List getSnapPreventions() { + return snapPreventions; + } + + public GHMatrixRequest setSnapPreventions(List snapPreventions) { + this.snapPreventions.addAll(snapPreventions); + return this; + } + + public List getOriginCurbsides() { + return originCurbsides; + } + + @JsonProperty("from_curbsides") + public GHMatrixRequest setOriginCurbsides(List curbsides) { + this.originCurbsides.addAll(curbsides); + return this; + } + + public List getDestinationCurbsides() { + return destinationCurbsides; + } + + @JsonProperty("to_curbside") + public GHMatrixRequest setDestinationCurbsides(List curbsides) { + this.destinationCurbsides.addAll(curbsides); + return this; + } + + public GHMatrixRequest setCurbsides(List curbsides) { + this.destinationCurbsides.addAll(curbsides); + this.originCurbsides.addAll(curbsides); + return this; + } + + public CustomModel getCustomModel() { + return customModel; + } + + public GHMatrixRequest setCustomModel(CustomModel customModel) { + this.customModel = customModel; + return this; } } diff --git a/core/src/main/java/com/graphhopper/routing/matrix/RouterMatrix.java b/core/src/main/java/com/graphhopper/routing/matrix/RouterMatrix.java index c5f121c0629..2d85b6838fb 100644 --- a/core/src/main/java/com/graphhopper/routing/matrix/RouterMatrix.java +++ b/core/src/main/java/com/graphhopper/routing/matrix/RouterMatrix.java @@ -29,6 +29,18 @@ public RouterMatrix(BaseGraph graph, EncodingManager encodingManager, LocationIn public GHMatrixResponse matrix(GHMatrixRequest request) { + if (!request.getOriginPointHints().isEmpty() && !request.getDestinationPointHints().isEmpty()) + throw new IllegalArgumentException("Point hints are not supported for Matrix"); + + if (!request.getSnapPreventions().isEmpty()) + throw new IllegalArgumentException("SnapPreventions are not supported for Matrix " + request.getSnapPreventions().toString()); + + if (!request.getOriginCurbsides().isEmpty() && !request.getDestinationCurbsides().isEmpty()) + throw new IllegalArgumentException("Curbsides are not supported for Matrix"); + + if (request.getCustomModel() != null) + throw new IllegalArgumentException("CustomModel not supported for Matrix: " + request.getCustomModel().toString()); + Profile profile = profilesByName.get(request.getProfile()); RoutingCHGraph chGraph = chGraphs.get(profile.getName()); From 83929c2d5cb79376d6af142c3e2d6697a32e0b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Rodrigo?= Date: Tue, 8 Oct 2024 23:07:38 +0200 Subject: [PATCH 2/2] Add MatrixResource for web API /matrix endpoint --- .../graphhopper/http/GraphHopperBundle.java | 1 + .../graphhopper/resources/MatrixResource.java | 228 ++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 web-bundle/src/main/java/com/graphhopper/resources/MatrixResource.java 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 701b8d0b29c..bbdaa487b40 100644 --- a/web-bundle/src/main/java/com/graphhopper/http/GraphHopperBundle.java +++ b/web-bundle/src/main/java/com/graphhopper/http/GraphHopperBundle.java @@ -280,6 +280,7 @@ protected void configure() { environment.jersey().register(MVTResource.class); environment.jersey().register(NearestResource.class); environment.jersey().register(RouteResource.class); + environment.jersey().register(MatrixResource.class); environment.jersey().register(IsochroneResource.class); environment.jersey().register(MapMatchingResource.class); if (configuration.getGraphHopperConfiguration().has("gtfs.file")) { diff --git a/web-bundle/src/main/java/com/graphhopper/resources/MatrixResource.java b/web-bundle/src/main/java/com/graphhopper/resources/MatrixResource.java new file mode 100644 index 00000000000..6e80394c82b --- /dev/null +++ b/web-bundle/src/main/java/com/graphhopper/resources/MatrixResource.java @@ -0,0 +1,228 @@ +/* + * 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.resources; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphhopper.routing.matrix.GHMatrixRequest; +import com.graphhopper.routing.matrix.GHMatrixResponse; +import com.graphhopper.routing.matrix.DistanceMatrix; +import com.graphhopper.GraphHopper; +import com.graphhopper.http.GHPointParam; +import com.graphhopper.http.GHRequestTransformer; +import com.graphhopper.http.ProfileResolver; +import com.graphhopper.jackson.MultiException; +import com.graphhopper.jackson.ResponsePathSerializer; +import com.graphhopper.util.*; +import com.graphhopper.util.shapes.GHPoint; +import io.dropwizard.jersey.params.AbstractParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.validation.constraints.NotNull; +import javax.ws.rs.*; +import javax.ws.rs.core.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static com.graphhopper.util.Parameters.Details.PATH_DETAILS; +import static com.graphhopper.util.Parameters.Routing.*; +import static java.util.stream.Collectors.toList; + +/** + * Resource to use GraphHopper in a remote client application like mobile or browser. Note: If type + * is json it returns the points in GeoJson array format [longitude,latitude] unlike the format "lat,lon" + * used for the request. See the full API response format in docs/web/api-doc.md + * + * @author Peter Karich + */ +@Path("matrix") +public class MatrixResource { + + private static final Logger logger = LoggerFactory.getLogger(MatrixResource.class); + + private final GraphHopper graphHopper; + private final ProfileResolver profileResolver; + private final GHRequestTransformer ghRequestTransformer; + private final Boolean hasElevation; + private final String osmDate; + + @Inject + public MatrixResource(GraphHopper graphHopper, ProfileResolver profileResolver, GHRequestTransformer ghRequestTransformer, @Named("hasElevation") Boolean hasElevation) { + this.graphHopper = graphHopper; + this.profileResolver = profileResolver; + this.ghRequestTransformer = ghRequestTransformer; + this.hasElevation = hasElevation; + this.osmDate = graphHopper.getProperties().get("datareader.data.date"); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response doGet( + @Context HttpServletRequest httpReq, + @Context UriInfo uriInfo, + @QueryParam("type") @DefaultValue("json") String type, + @QueryParam(WAY_POINT_MAX_DISTANCE) @DefaultValue("1") double minPathPrecision, + @QueryParam(ALGORITHM) @DefaultValue("") String algoStr, + @QueryParam("profile") String profileName, + @QueryParam("point") @NotNull List pointParams, + @QueryParam("from_point") @NotNull List fromPointParams, + @QueryParam("to_point") @NotNull List toPointParams, + @QueryParam(POINT_HINT) List pointHints, + @QueryParam("from_point_hint") List fromPointHints, + @QueryParam("to_point_hint") List toPointHints, + @QueryParam(SNAP_PREVENTION) List snapPreventions, + @QueryParam(CURBSIDE) List curbsides, + @QueryParam("from_curbside") List fromCurbsides, + @QueryParam("to_curbside") List toCurbsides, + @QueryParam("out_array") List outArrays, + @QueryParam("fail_fast") @DefaultValue("true") String failFast) { + StopWatch sw = new StopWatch().start(); + List points = pointParams.stream().map(AbstractParam::get).collect(toList()); + List fromPoints = fromPointParams.stream().map(AbstractParam::get).collect(toList()); + List toPoints = toPointParams.stream().map(AbstractParam::get).collect(toList()); + + GHMatrixRequest request = new GHMatrixRequest(); + initHints(request.getHints(), uriInfo.getQueryParameters()); + + request.setAlgorithm(algoStr). + setProfile(profileName); + if (points != null) { + request.setOrigins(points). + setDestinations(points); + } else { + request.setOrigins(fromPoints). + setDestinations(toPoints); + } + if (pointHints != null) { + request.setOriginPointHints(pointHints). + setDestinationPointHints(pointHints); + } else { + request.setOriginPointHints(fromPointHints). + setDestinationPointHints(toPointHints); + } + request.setSnapPreventions(snapPreventions); + if (curbsides != null) { + request.setOriginCurbsides(curbsides). + setDestinationCurbsides(curbsides); + } else { + request.setOriginCurbsides(fromCurbsides). + setDestinationCurbsides(toCurbsides); + } + request.setFailFast(Boolean.parseBoolean(failFast)); + + // request = ghRequestTransformer.transformRequest(request); + + PMap profileResolverHints = new PMap(request.getHints()); + profileResolverHints.putObject("profile", profileName); + profileName = profileResolver.resolveProfile(profileResolverHints); + removeLegacyParameters(request.getHints()); + request.setProfile(profileName); + + GHMatrixResponse ghResponse = graphHopper.matrix(request); + + double took = sw.stop().getMillisDouble(); + String logStr = (httpReq.getRemoteAddr() + " " + httpReq.getLocale() + " " + httpReq.getHeader("User-Agent")) + " " + fromPoints + " " + toPoints + ", took: " + String.format("%.1f", took) + "ms, algo: " + algoStr + ", profile: " + profileName; + logger.info(logStr); + + DistanceMatrix matrix = ghResponse.getMatrix(); + ObjectNode json = JsonNodeFactory.instance.objectNode(); + final ObjectNode info = json.putObject("info"); + info.putPOJO("copyrights", ResponsePathSerializer.COPYRIGHTS); + info.put("took", Math.round((float) sw.getMillis())); + if (outArrays != null && outArrays.contains("times")) { + json.putPOJO("times", matrix.getTimes()); + } + if (outArrays != null && outArrays.contains("distances")) { + json.putPOJO("distances", matrix.getDistances()); + } + + return Response.ok(json). + header("X-GH-Took", "" + Math.round(took)). + type(MediaType.APPLICATION_JSON). + build(); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response doPost(@NotNull GHMatrixRequest request, @Context HttpServletRequest httpReq) { + StopWatch sw = new StopWatch().start(); + // request = ghRequestTransformer.transformRequest(request); + + PMap profileResolverHints = new PMap(request.getHints()); + profileResolverHints.putObject("profile", request.getProfile()); + request.setProfile(profileResolver.resolveProfile(profileResolverHints)); + removeLegacyParameters(request.getHints()); + + GHMatrixResponse ghResponse = graphHopper.matrix(request); + List outArrays = request.getHints().getObject("out_arrays", new ArrayList()); + + double took = sw.stop().getMillisDouble(); + String infoStr = httpReq.getRemoteAddr() + " " + httpReq.getLocale() + " " + httpReq.getHeader("User-Agent"); + String logStr = infoStr + " " + request.getOrigins().size() + "x" + request.getDestinations().size() + ", took: " + + String.format("%.1f", took) + " ms, algo: " + request.getAlgorithm() + ", profile: " + request.getProfile() + + ", custom_model: " + request.getCustomModel(); + logger.info(logStr); + + DistanceMatrix matrix = ghResponse.getMatrix(); + ObjectNode json = JsonNodeFactory.instance.objectNode(); + final ObjectNode info = json.putObject("info"); + info.putPOJO("copyrights", ResponsePathSerializer.COPYRIGHTS); + info.put("took", Math.round((float) sw.getMillis())); + if (outArrays != null && outArrays.contains("times")) { + json.putPOJO("times", matrix.getTimes()); + } + if (outArrays != null && outArrays.contains("distances")) { + json.putPOJO("distances", matrix.getDistances()); + } + + return Response.ok(json). + header("X-GH-Took", "" + Math.round(took)). + type(MediaType.APPLICATION_JSON). + build(); + } + + public static void removeLegacyParameters(PMap hints) { + // these parameters should only be used to resolve the profile, but should not be passed to GraphHopper + hints.remove("weighting"); + hints.remove("vehicle"); + hints.remove("edge_based"); + hints.remove("turn_costs"); + } + + static void initHints(PMap m, MultivaluedMap parameterMap) { + for (Map.Entry> e : parameterMap.entrySet()) { + if (e.getValue().size() == 1) { + m.putObject(Helper.camelCaseToUnderScore(e.getKey()), Helper.toObject(e.getValue().get(0))); + } else { + // TODO e.g. 'point' parameter occurs multiple times and we cannot throw an exception here + // unknown parameters (hints) should be allowed to be multiparameters, too, or we shouldn't use them for + // known parameters either, _or_ known parameters must be filtered before they come to this code point, + // _or_ we stop passing unknown parameters altogether. + // throw new WebApplicationException(String.format("This query parameter (hint) is not allowed to occur multiple times: %s", e.getKey())); + // see also #1976 + } + } + } +}