/*
* Geopaparazzi - Digital field mapping on Android based devices
* Copyright (C) 2016 HydroloGIS (www.hydrologis.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.geopaparazzi.library.routing.osmbonuspack;
import android.content.Context;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import eu.geopaparazzi.library.R;
/** get a route between a start and a destination point, going through a list of waypoints.
* It uses OSRM, a free open source routing service based on OpenSteetMap data. <br>
*
* It requests by default the OSRM demo site.
* Use setService() to request an other (for instance your own) OSRM service. <br>
*
* @see <a href="https://github.com/DennisOSRM/Project-OSRM/wiki/Server-api">OSRM</a>
* @see <a href="https://github.com/Project-OSRM/osrm-backend/wiki/New-Server-api">V5 API</a>
*
* @author M.Kergall
*/
public class OSRMRoadManager extends RoadManager {
static final String SERVICE = "http://router.project-osrm.org/route/v1/driving/";
private final Context mContext;
protected String mServiceUrl;
protected String mUserAgent;
/**
* mapping from OSRM StepManeuver types to MapQuest maneuver IDs:
*/
static final HashMap<String, Integer> MANEUVERS;
static {
MANEUVERS = new HashMap<>();
MANEUVERS.put("new name", 2); //road name change
MANEUVERS.put("turn-straight", 1); //Continue straight
MANEUVERS.put("turn-slight right", 6); //Slight right
MANEUVERS.put("turn-right", 7); //Right
MANEUVERS.put("turn-sharp right", 8); //Sharp right
MANEUVERS.put("turn-uturn", 12); //U-turn
MANEUVERS.put("turn-sharp left", 5); //Sharp left
MANEUVERS.put("turn-left", 4); //Left
MANEUVERS.put("turn-slight left", 3); //Slight left
MANEUVERS.put("depart", 24); //"Head" => used by OSRM as the start node. Considered here as a "waypoint".
// TODO - to check...
MANEUVERS.put("arrive", 24); //Arrived (at waypoint)
MANEUVERS.put("roundabout-1", 27); //Round-about, 1st exit
MANEUVERS.put("roundabout-2", 28); //2nd exit, etc ...
MANEUVERS.put("roundabout-3", 29);
MANEUVERS.put("roundabout-4", 30);
MANEUVERS.put("roundabout-5", 31);
MANEUVERS.put("roundabout-6", 32);
MANEUVERS.put("roundabout-7", 33);
MANEUVERS.put("roundabout-8", 34); //Round-about, 8th exit
//TODO: other OSRM types to handle properly:
MANEUVERS.put("merge-left", 20);
MANEUVERS.put("merge-sharp left", 20);
MANEUVERS.put("merge-slight left", 20);
MANEUVERS.put("merge-right", 21);
MANEUVERS.put("merge-sharp right", 21);
MANEUVERS.put("merge-slight right", 21);
MANEUVERS.put("merge-straight", 22);
MANEUVERS.put("ramp-left", 17);
MANEUVERS.put("ramp-sharp left", 17);
MANEUVERS.put("ramp-slight left", 17);
MANEUVERS.put("ramp-right", 18);
MANEUVERS.put("ramp-sharp right", 18);
MANEUVERS.put("ramp-slight right", 18);
MANEUVERS.put("ramp-straight", 19);
//MANEUVERS.put("fork", );
//MANEUVERS.put("end of road", );
//MANEUVERS.put("continue", );
}
//From: Project-OSRM-Web / WebContent / localization / OSRM.Locale.en.js
// driving directions
// %s: road name
// %d: direction => removed
// <*>: will only be printed when there actually is a road name
static final HashMap<Integer, Object> DIRECTIONS;
static {
DIRECTIONS = new HashMap<>();
DIRECTIONS.put(1, R.string.osmbonuspack_directions_1);
DIRECTIONS.put(2, R.string.osmbonuspack_directions_2);
DIRECTIONS.put(3, R.string.osmbonuspack_directions_3);
DIRECTIONS.put(4, R.string.osmbonuspack_directions_4);
DIRECTIONS.put(5, R.string.osmbonuspack_directions_5);
DIRECTIONS.put(6, R.string.osmbonuspack_directions_6);
DIRECTIONS.put(7, R.string.osmbonuspack_directions_7);
DIRECTIONS.put(8, R.string.osmbonuspack_directions_8);
DIRECTIONS.put(12, R.string.osmbonuspack_directions_12);
DIRECTIONS.put(17, R.string.osmbonuspack_directions_17);
DIRECTIONS.put(18, R.string.osmbonuspack_directions_18);
DIRECTIONS.put(19, R.string.osmbonuspack_directions_19);
//DIRECTIONS.put(20, R.string.osmbonuspack_directions_20);
//DIRECTIONS.put(21, R.string.osmbonuspack_directions_21);
//DIRECTIONS.put(22, R.string.osmbonuspack_directions_22);
DIRECTIONS.put(24, R.string.osmbonuspack_directions_24);
DIRECTIONS.put(27, R.string.osmbonuspack_directions_27);
DIRECTIONS.put(28, R.string.osmbonuspack_directions_28);
DIRECTIONS.put(29, R.string.osmbonuspack_directions_29);
DIRECTIONS.put(30, R.string.osmbonuspack_directions_30);
DIRECTIONS.put(31, R.string.osmbonuspack_directions_31);
DIRECTIONS.put(32, R.string.osmbonuspack_directions_32);
DIRECTIONS.put(33, R.string.osmbonuspack_directions_33);
DIRECTIONS.put(34, R.string.osmbonuspack_directions_34);
}
public OSRMRoadManager(Context context){
super();
mContext = context;
mServiceUrl = SERVICE;
mUserAgent = BonusPackHelper.DEFAULT_USER_AGENT; //set user agent to the default one.
}
/** allows to request on an other site than OSRM demo site */
public void setService(String serviceUrl){
mServiceUrl = serviceUrl;
}
/** allows to send to OSRM service a user agent specific to the app,
* instead of the default user agent of OSMBonusPack lib.
*/
public void setUserAgent(String userAgent){
mUserAgent = userAgent;
}
public String getUrl(ArrayList<GeoPoint> waypoints, boolean getAlternate) {
StringBuilder urlString = new StringBuilder(mServiceUrl);
for (int i=0; i<waypoints.size(); i++){
GeoPoint p = waypoints.get(i);
if (i>0)
urlString.append(';');
urlString.append(Double.toString(p.getLongitude()) + "," + Double.toString(p.getLatitude()));
}
urlString.append("?alternatives="+(getAlternate?"true" : "false"));
urlString.append("&overview=full&steps=true");
urlString.append(mOptions);
return urlString.toString();
}
/*
protected void getInstructions(Road road, JSONArray jInstructions){
try {
int n = jInstructions.length();
RoadNode lastNode = null;
for (int i=0; i<n; i++){
JSONArray jInstruction = jInstructions.getJSONArray(i);
RoadNode node = new RoadNode();
int positionIndex = jInstruction.getInt(3);
node.mLocation = road.mRouteHigh.get(positionIndex);
node.mLength = jInstruction.getInt(2)/1000.0;
node.mDuration = jInstruction.getInt(4); //Segment duration in seconds.
String direction = jInstruction.getString(0);
String roadName = jInstruction.getString(1);
if (lastNode!=null && "1".equals(direction) && "".equals(roadName)){
//node "Continue" with no road name is useless, don't add it
lastNode.mLength += node.mLength;
lastNode.mDuration += node.mDuration;
} else {
node.mManeuverType = getManeuverCode(direction);
node.mInstructions = buildInstructions(direction, roadName);
//Log.d(BonusPackHelper.LOG_TAG, direction+"=>"+node.mManeuverType+"; "+node.mInstructions);
road.mNodes.add(node);
lastNode = node;
}
}
} catch (JSONException e) {
road.mStatus = Road.STATUS_TECHNICAL_ISSUE;
e.printStackTrace();
}
}
*/
protected Road[] defaultRoad(ArrayList<GeoPoint> waypoints){
Road[] roads = new Road[1];
roads[0] = new Road(waypoints);
return roads;
}
protected Road[] getRoads(ArrayList<GeoPoint> waypoints, boolean getAlternate) {
String url = getUrl(waypoints, getAlternate);
Log.d(BonusPackHelper.LOG_TAG, "OSRMRoadManager.getRoads:" + url);
//url = "http://comob.free.fr/osrm_sample.json"; //DEBUG - waiting for OSRM V5 live
String jString = BonusPackHelper.requestStringFromUrl(url, mUserAgent);
if (jString == null) {
Log.e(BonusPackHelper.LOG_TAG, "OSRMRoadManager::getRoad: request failed.");
return null;
}
try {
JSONObject jObject = new JSONObject(jString);
String jCode = jObject.getString("code");
if (!"Ok".equals(jCode)) {
Log.e(BonusPackHelper.LOG_TAG, "OSRMRoadManager::getRoad: error code=" + jCode);
Road[] roads = defaultRoad(waypoints);
if ("NoRoute".equals(jCode)) {
roads[0].mStatus = Road.STATUS_INVALID;
}
return roads;
} else {
JSONArray jRoutes = jObject.getJSONArray("routes");
Road[] roads = new Road[jRoutes.length()];
for (int i=0; i<jRoutes.length(); i++){
Road road = new Road();
roads[i] = road;
road.mStatus = Road.STATUS_OK;
JSONObject jRoute = jRoutes.getJSONObject(i);
String route_geometry = jRoute.getString("geometry");
road.mRouteHigh = PolylineEncoder.decode(route_geometry, 10, false);
road.mBoundingBox = BoundingBox.fromGeoPoints(road.mRouteHigh);
road.mLength = jRoute.getDouble("distance") / 1000.0;
road.mDuration = jRoute.getDouble("duration");
//legs:
JSONArray jLegs = jRoute.getJSONArray("legs");
for (int l=0; l<jLegs.length(); l++) {
//leg:
JSONObject jLeg = jLegs.getJSONObject(l);
RoadLeg leg = new RoadLeg();
road.mLegs.add(leg);
leg.mLength = jLeg.getDouble("distance");
leg.mDuration = jLeg.getDouble("duration");
//steps:
JSONArray jSteps = jLeg.getJSONArray("steps");
RoadNode lastNode = null;
String lastRoadName = "";
for (int s=0; s<jSteps.length(); s++) {
JSONObject jStep = jSteps.getJSONObject(s);
RoadNode node = new RoadNode();
node.mLength = jStep.getDouble("distance") / 1000.0;
node.mDuration = jStep.getDouble("duration");
JSONObject jStepManeuver = jStep.getJSONObject("maneuver");
JSONArray jLocation = jStepManeuver.getJSONArray("location");
node.mLocation = new GeoPoint(jLocation.getDouble(1), jLocation.getDouble(0));
String direction = jStepManeuver.getString("type");
if (direction.equals("turn") || direction.equals("ramp") || direction.equals("merge")){
String modifier = jStepManeuver.getString("modifier");
direction = direction + '-' + modifier;
} else if (direction.equals("roundabout")){
int exit = jStepManeuver.getInt("exit");
direction = direction + '-' + exit;
} else if (direction.equals("rotary")) {
int exit = jStepManeuver.getInt("exit");
direction = "roundabout" + '-' + exit; //convert rotary in roundabout...
}
node.mManeuverType = getManeuverCode(direction);
String roadName = jStep.optString("name", "");
node.mInstructions = buildInstructions(node.mManeuverType, roadName);
if (lastNode != null && node.mManeuverType == 2 && lastRoadName.equals(roadName)) {
//workaround for https://github.com/Project-OSRM/osrm-backend/issues/2273
//"new name", but identical to previous name:
//skip, but update values of last node:
lastNode.mDuration += node.mDuration;
lastNode.mLength += node.mLength;
} else {
road.mNodes.add(node);
lastNode = node;
lastRoadName = roadName;
}
} //steps
} //legs
} //routes
Log.d(BonusPackHelper.LOG_TAG, "OSRMRoadManager.getRoads - finished");
return roads;
} //if code is Ok
} catch (JSONException e) {
e.printStackTrace();
return null;
}
}
@Override public Road[] getRoads(ArrayList<GeoPoint> waypoints) {
return getRoads(waypoints, true);
}
@Override public Road getRoad(ArrayList<GeoPoint> waypoints) {
Road[] roads = getRoads(waypoints, false);
if (roads == null) return null;
return roads[0];
}
protected int getManeuverCode(String direction){
Integer code = MANEUVERS.get(direction);
if (code != null)
return code;
else
return 0;
}
protected String buildInstructions(int maneuver, String roadName){
Integer resDirection = (Integer) DIRECTIONS.get(maneuver);
if (resDirection == null)
return null;
String direction = mContext.getString(resDirection);
String instructions;
if (roadName.equals(""))
//remove "<*>"
instructions = direction.replaceFirst("\\[[^\\]]*\\]", "");
else {
direction = direction.replace('[', ' ');
direction = direction.replace(']', ' ');
instructions = String.format(direction, roadName);
}
return instructions;
}
}