/*
* 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.matching.http;
import com.fasterxml.jackson.databind.JsonNode;
import com.graphhopper.GHResponse;
import com.graphhopper.GraphHopper;
import com.graphhopper.PathWrapper;
import com.graphhopper.http.GraphHopperServlet;
import com.graphhopper.http.RouteSerializer;
import com.graphhopper.matching.EdgeMatch;
import com.graphhopper.matching.GPXFile;
import com.graphhopper.matching.MapMatching;
import com.graphhopper.matching.MatchResult;
import com.graphhopper.routing.AlgorithmOptions;
import com.graphhopper.routing.Path;
import com.graphhopper.routing.util.HintsMap;
import com.graphhopper.util.*;
import com.graphhopper.util.Parameters.Routing;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
/**
*
* @author Peter Karich
*/
public class MatchServlet extends GraphHopperServlet {
private static final String GPX_FORMAT = "gpx";
private static final String EXTENDED_JSON_FORMAT = "extended_json";
@Inject
private GraphHopper hopper;
@Inject
private RouteSerializer routeSerializer;
@Inject
private TranslationMap trMap;
@Inject
@Named("gps.max_accuracy")
private double gpsMaxAccuracy;
@Override
public void doPost(HttpServletRequest httpReq, HttpServletResponse httpRes)
throws ServletException, IOException {
String infoStr = httpReq.getRemoteAddr() + " " + httpReq.getLocale();
String inType = "gpx";
String contentType = httpReq.getContentType();
if (contentType.contains("application/xml") || contentType.contains("application/gpx+xml")) {
inType = "gpx";
} else if (contentType.contains("application/json")) {
inType = "json";
}
PathWrapper matchGHRsp = new PathWrapper();
final String outType = getParam(httpReq, "type", "json");
GPXFile gpxFile = new GPXFile();
if (inType.equals("gpx")) {
try {
gpxFile = parseGPX(httpReq);
} catch (Exception ex) {
matchGHRsp.addError(ex);
}
} else {
matchGHRsp.addError(new IllegalArgumentException("Input type not supported " + inType + ", Content-Type:" + contentType));
}
boolean writeGPX = GPX_FORMAT.equals(outType);
boolean pointsEncoded = getBooleanParam(httpReq, "points_encoded", true);
boolean enableInstructions = writeGPX || getBooleanParam(httpReq, "instructions", true);
boolean enableElevation = getBooleanParam(httpReq, "elevation", false);
// TODO export OSM IDs instead, use https://github.com/karussell/graphhopper-osm-id-mapping
boolean enableTraversalKeys = getBooleanParam(httpReq, "traversal_keys", false);
String vehicle = getParam(httpReq, "vehicle", "car");
int maxVisitedNodes = Math.min(getIntParam(httpReq, Routing.MAX_VISITED_NODES, 3000), 5000);
double wayPointMaxDistance = getDoubleParam(httpReq, Routing.WAY_POINT_MAX_DISTANCE, 1d);
double defaultAccuracy = 40;
double gpsAccuracy = Math.min(Math.max(getDoubleParam(httpReq, "gps_accuracy", defaultAccuracy), 5), gpsMaxAccuracy);
Locale locale = Helper.getLocale(getParam(httpReq, "locale", "en"));
MatchResult matchRsp = null;
StopWatch sw = new StopWatch().start();
if (!matchGHRsp.hasErrors()) {
try {
AlgorithmOptions opts = AlgorithmOptions.start()
.traversalMode(hopper.getTraversalMode())
.maxVisitedNodes(maxVisitedNodes)
.hints(new HintsMap().put("vehicle", vehicle))
.build();
MapMatching matching = new MapMatching(hopper, opts);
matching.setMeasurementErrorSigma(gpsAccuracy);
matchRsp = matching.doWork(gpxFile.getEntries());
// fill GHResponse for identical structure
Path path = matching.calcPath(matchRsp);
Translation tr = trMap.getWithFallBack(locale);
DouglasPeucker peucker = new DouglasPeucker().setMaxDistance(wayPointMaxDistance);
PathMerger pathMerger = new PathMerger().
setDouglasPeucker(peucker).
setSimplifyResponse(wayPointMaxDistance > 0);
pathMerger.doWork(matchGHRsp, Collections.singletonList(path), tr);
} catch (Exception ex) {
matchGHRsp.addError(ex);
}
}
float took = sw.stop().getSeconds();
httpRes.setHeader("X-GH-Took", "" + Math.round(took * 1000));
if (EXTENDED_JSON_FORMAT.equals(outType)) {
if (matchGHRsp.hasErrors()) {
httpRes.setStatus(SC_BAD_REQUEST);
objectMapper.writeValue(httpRes.getWriter(), matchGHRsp.getErrors());
} else {
httpRes.getWriter().write(objectMapper.writeValueAsString(MatchResultToJson.convertToTree(matchRsp, objectMapper)));
}
} else if (GPX_FORMAT.equals(outType)) {
if (matchGHRsp.hasErrors()) {
httpRes.setStatus(SC_BAD_REQUEST);
httpRes.getWriter().append(errorsToXML(matchGHRsp.getErrors()));
} else {
String xml = createGPXString(httpReq, httpRes, matchGHRsp);
writeResponse(httpRes, xml);
}
} else {
GHResponse rsp = new GHResponse();
rsp.add(matchGHRsp);
Map<String, Object> map = routeSerializer.toJSON(rsp, true, pointsEncoded,
enableElevation, enableInstructions);
if (rsp.hasErrors()) {
writeJsonError(httpRes, SC_BAD_REQUEST, objectMapper.convertValue(map, JsonNode.class));
} else {
if (matchRsp == null) {
throw new IllegalStateException("match response has to be none-null if no error happened");
}
Map<String, Object> matchResult = new HashMap<String, Object>();
matchResult.put("distance", matchRsp.getMatchLength());
matchResult.put("time", matchRsp.getMatchMillis());
matchResult.put("original_distance", matchRsp.getGpxEntriesLength());
matchResult.put("original_time", matchRsp.getGpxEntriesMillis());
map.put("map_matching", matchResult);
if (enableTraversalKeys) {
// encode edges as traversal keys which includes orientation
// decode simply by multiplying with 0.5
List<Integer> traversalKeylist = new ArrayList<Integer>();
for (EdgeMatch em : matchRsp.getEdgeMatches()) {
EdgeIteratorState edge = em.getEdgeState();
traversalKeylist.add(GHUtility.createEdgeKey(edge.getBaseNode(), edge.getAdjNode(), edge.getEdge(), false));
}
map.put("traversal_keys", traversalKeylist);
}
writeJson(httpReq, httpRes, objectMapper.convertValue(map, JsonNode.class));
}
}
String str = httpReq.getQueryString() + ", " + infoStr + ", took:" + took + ", entries:" + gpxFile.getEntries().size() + ", " + matchGHRsp.getDebugInfo();
if (matchGHRsp.hasErrors()) {
if (matchGHRsp.getErrors().get(0) instanceof IllegalArgumentException) {
logger.error(str + ", errors:" + matchGHRsp.getErrors());
} else {
logger.error(str + ", errors:" + matchGHRsp.getErrors(), matchGHRsp.getErrors().get(0));
}
} else {
logger.info(str);
}
}
private GPXFile parseGPX(HttpServletRequest httpReq) throws IOException {
GPXFile file = new GPXFile();
return file.doImport(httpReq.getInputStream(), 20);
}
}