/*
* 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.http;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.graphhopper.GHRequest;
import com.graphhopper.GHResponse;
import com.graphhopper.GraphHopperAPI;
import com.graphhopper.PathWrapper;
import com.graphhopper.util.*;
import com.graphhopper.util.exceptions.*;
import com.graphhopper.util.shapes.GHPoint;
import java.util.*;
import java.util.Map.Entry;
/**
* Main wrapper of the GraphHopper Directions API for a simple and efficient usage.
* <p>
*
* @author Peter Karich
*/
public class GraphHopperWeb implements GraphHopperAPI {
private ObjectMapper objectMapper;
private final Set<String> ignoreSet;
private Downloader downloader = new Downloader("GraphHopper Java Client");
private String routeServiceUrl = "https://graphhopper.com/api/1/route";
private String key = "";
private boolean instructions = true;
private boolean calcPoints = true;
private boolean turnDescription = true;
private boolean elevation = false;
public GraphHopperWeb() {
// some parameters are supported directly via Java API so ignore them when writing the getHints map
ignoreSet = new HashSet<>();
ignoreSet.add("calc_points");
ignoreSet.add("calcpoints");
ignoreSet.add("instructions");
ignoreSet.add("elevation");
ignoreSet.add("key");
// some parameters are in the request:
ignoreSet.add("algorithm");
ignoreSet.add("locale");
ignoreSet.add("point");
ignoreSet.add("vehicle");
// some are special and need to be avoided
ignoreSet.add("points_encoded");
ignoreSet.add("pointsencoded");
ignoreSet.add("type");
objectMapper = new ObjectMapper();
}
private PathWrapper createPathWrapper(JsonNode path,
boolean tmpCalcPoints, boolean tmpInstructions,
boolean tmpElevation, boolean turnDescription) {
PathWrapper pathWrapper = new PathWrapper();
pathWrapper.addErrors(readErrors(path));
if (pathWrapper.hasErrors())
return pathWrapper;
if (path.has("snapped_waypoints")) {
String snappedPointStr = path.get("snapped_waypoints").asText();
PointList snappedPoints = WebHelper.decodePolyline(snappedPointStr, 5, tmpElevation);
pathWrapper.setWaypoints(snappedPoints);
}
if (tmpCalcPoints) {
String pointStr = path.get("points").asText();
PointList pointList = WebHelper.decodePolyline(pointStr, 100, tmpElevation);
pathWrapper.setPoints(pointList);
if (tmpInstructions) {
JsonNode instrArr = path.get("instructions");
InstructionList il = new InstructionList(null);
int viaCount = 1;
for (JsonNode jsonObj : instrArr) {
double instDist = jsonObj.get("distance").asDouble();
String text = turnDescription ? jsonObj.get("text").asText() : jsonObj.get("street_name").asText();
long instTime = jsonObj.get("time").asLong();
int sign = jsonObj.get("sign").asInt();
JsonNode iv = jsonObj.get("interval");
int from = iv.get(0).asInt();
int to = iv.get(1).asInt();
PointList instPL = new PointList(to - from, tmpElevation);
for (int j = from; j <= to; j++) {
instPL.add(pointList, j);
}
InstructionAnnotation ia = InstructionAnnotation.EMPTY;
if (jsonObj.has("annotation_importance") && jsonObj.has("annotation_text")) {
ia = new InstructionAnnotation(jsonObj.get("annotation_importance").asInt(), jsonObj.get("annotation_text").asText());
}
Instruction instr;
if (sign == Instruction.USE_ROUNDABOUT || sign == Instruction.LEAVE_ROUNDABOUT) {
RoundaboutInstruction ri = new RoundaboutInstruction(sign, text, ia, instPL);
if (jsonObj.has("exit_number")) {
ri.setExitNumber(jsonObj.get("exit_number").asInt());
}
if (jsonObj.has("exited")) {
if (jsonObj.get("exited").asBoolean())
ri.setExited();
}
if (jsonObj.has("turn_angle")) {
// TODO provide setTurnAngle setter
double angle = jsonObj.get("turn_angle").asDouble();
ri.setDirOfRotation(angle);
ri.setRadian((angle < 0 ? -Math.PI : Math.PI) - angle);
}
instr = ri;
} else if (sign == Instruction.REACHED_VIA) {
ViaInstruction tmpInstr = new ViaInstruction(text, ia, instPL);
tmpInstr.setViaCount(viaCount);
viaCount++;
instr = tmpInstr;
} else if (sign == Instruction.FINISH) {
instr = new FinishInstruction(instPL, 0);
} else {
instr = new Instruction(sign, text, ia, instPL);
}
// Usually, the translation is done from the routing service so just use the provided string
// instead of creating a combination with sign and name etc.
// This is called the turn description.
// This can be changed by passing <code>turn_description=false</code>.
if(turnDescription)
instr.setUseRawName();
instr.setDistance(instDist).setTime(instTime);
il.add(instr);
}
pathWrapper.setInstructions(il);
}
}
double distance = path.get("distance").asDouble();
long time = path.get("time").asLong();
pathWrapper.setDistance(distance).setTime(time);
return pathWrapper;
}
// Credits to: http://stackoverflow.com/a/24012023/194609
private Map<String, Object> toMap(JsonNode object) {
return objectMapper.convertValue(object, new TypeReference<Map<String, Object>>() {});
}
public List<Throwable> readErrors(JsonNode json) {
List<Throwable> errors = new ArrayList<>();
JsonNode errorJson;
if (json.has("message")) {
if (json.has("hints")) {
errorJson = json.get("hints");
} else {
// should not happen
errors.add(new RuntimeException(json.get("message").asText()));
return errors;
}
} else
return errors;
for (JsonNode error : errorJson) {
String exClass = "";
if (error.has("details"))
exClass = error.get("details").asText();
String exMessage = error.get("message").asText();
if (exClass.equals(UnsupportedOperationException.class.getName()))
errors.add(new UnsupportedOperationException(exMessage));
else if (exClass.equals(IllegalStateException.class.getName()))
errors.add(new IllegalStateException(exMessage));
else if (exClass.equals(RuntimeException.class.getName()))
errors.add(new DetailedRuntimeException(exMessage, toMap(error)));
else if (exClass.equals(IllegalArgumentException.class.getName()))
errors.add(new DetailedIllegalArgumentException(exMessage, toMap(error)));
else if (exClass.equals(ConnectionNotFoundException.class.getName())) {
errors.add(new ConnectionNotFoundException(exMessage, toMap(error)));
} else if (exClass.equals(PointNotFoundException.class.getName())) {
int pointIndex = error.get("point_index").asInt();
errors.add(new PointNotFoundException(exMessage, pointIndex));
} else if (exClass.equals(PointOutOfBoundsException.class.getName())) {
int pointIndex = error.get("point_index").asInt();
errors.add(new PointOutOfBoundsException(exMessage, pointIndex));
} else if (exClass.isEmpty())
errors.add(new DetailedRuntimeException(exMessage, toMap(error)));
else
errors.add(new DetailedRuntimeException(exClass + " " + exMessage, toMap(error)));
}
if (json.has("message") && errors.isEmpty())
errors.add(new RuntimeException(json.get("message").asText()));
return errors;
}
public GraphHopperWeb setDownloader(Downloader downloader) {
this.downloader = downloader;
return this;
}
@Override
public boolean load(String serviceUrl) {
this.routeServiceUrl = serviceUrl;
return true;
}
public GraphHopperWeb setKey(String key) {
if (key == null || key.isEmpty())
throw new IllegalStateException("Key cannot be empty");
this.key = key;
return this;
}
public GraphHopperWeb setCalcPoints(boolean calcPoints) {
this.calcPoints = calcPoints;
return this;
}
public GraphHopperWeb setInstructions(boolean b) {
instructions = b;
return this;
}
public GraphHopperWeb setElevation(boolean withElevation) {
this.elevation = withElevation;
return this;
}
@Override
public GHResponse route(GHRequest request) {
try {
String places = "";
for (GHPoint p : request.getPoints()) {
places += "point=" + p.lat + "," + p.lon + "&";
}
boolean tmpInstructions = request.getHints().getBool("instructions", instructions);
boolean tmpCalcPoints = request.getHints().getBool("calc_points", calcPoints);
boolean tmpTurnDescription = request.getHints().getBool("turn_description", turnDescription);
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);
String url = routeServiceUrl
+ "?"
+ places
+ "&type=json"
+ "&instructions=" + tmpInstructions
+ "&points_encoded=true"
+ "&calc_points=" + tmpCalcPoints
+ "&algorithm=" + request.getAlgorithm()
+ "&locale=" + request.getLocale().toString()
+ "&elevation=" + tmpElevation;
if (!request.getVehicle().isEmpty())
url += "&vehicle=" + request.getVehicle();
if (!key.isEmpty())
url += "&key=" + key;
for (Entry<String, String> entry : request.getHints().toMap().entrySet()) {
String urlKey = entry.getKey();
String urlValue = entry.getValue();
// use lower case conversion for check only!
if (ignoreSet.contains(urlKey.toLowerCase()))
continue;
if (urlValue != null && !urlValue.isEmpty())
url += "&" + WebHelper.encodeURL(urlKey) + "=" + WebHelper.encodeURL(urlValue);
}
String str = downloader.downloadAsString(url, true);
JsonNode json = objectMapper.reader().readTree(str);
GHResponse res = new GHResponse();
res.addErrors(readErrors(json));
if (res.hasErrors())
return res;
JsonNode paths = json.get("paths");
for (JsonNode path : paths) {
PathWrapper altRsp = createPathWrapper(path, tmpCalcPoints, tmpInstructions, tmpElevation, tmpTurnDescription);
res.add(altRsp);
}
return res;
} catch (Exception ex) {
throw new RuntimeException("Problem while fetching path " + request.getPoints() + ": " + ex.getMessage(), ex);
}
}
}