How do I draw a route, along an existing road, between two points?

Viewed 86248

I want to show the driving route between two locations in my android app. I want to draw the route only on top of road segments.

There are several answers on stack overflow itself, and all of them were using the same method. Get the directions from start point to destination using google directions API, and draw a polyline across the points returned. Following are some of the answers which uses this method.

https://stackoverflow.com/a/17007360/1015678

https://stackoverflow.com/a/40563930/1015678

But, problem with above method is, when the roads are not straight, the dawn rote is not always on top of the roads, because directions API only returns points where you need to turn from one road to another (at junctions). It doesn't give point details in the bends of the same road segment. So, when I use above method in an area where the roads have so many bends, the route drawn almost always is not on top of road segments.

I found this answer, which does what I need to do, using the javascript API. In this solution, the drawn route nicely follows the roads, similar to the google maps android app. Does someone know whether this is achievable in an android app?

Google Maps android app can nicely draw a route from one point to another, keeping the route on the roads. Does anyone know how Google Maps is doing this? Is it using any other API which is not publicly exposed?

5 Answers

Enable Direction API from Google Console. Replace API_KEY in GetPathFromLocation.java class

import android.graphics.Color;
import android.os.AsyncTask;
import android.util.Log;

import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.PolylineOptions;

import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class GetPathFromLocation extends AsyncTask<String, Void, PolylineOptions> {

    private String TAG = "GetPathFromLocation";
    private String API_KEY = "Place_Your_API_Key";
    private LatLng source, destination;
    private DirectionPointListener resultCallback;

    public GetPathFromLocation(LatLng source, LatLng destination, DirectionPointListener resultCallback) {
        this.source = source;
        this.destination = destination;
        this.resultCallback = resultCallback;
    }

    public String getUrl(LatLng origin, LatLng dest) {

        String str_origin = "origin=" + origin.latitude + "," + origin.longitude;
        String str_dest = "destination=" + dest.latitude + "," + dest.longitude;
        String sensor = "sensor=false";
        String parameters = str_origin + "&" + str_dest + "&" + sensor;
        String output = "json";
        String url = "https://maps.googleapis.com/maps/api/directions/" + output + "?" + parameters + "&key=" + API_KEY;

        return url;
    }

    @Override
    protected PolylineOptions doInBackground(String... url) {

        String data;

        try {
            InputStream inputStream = null;
            HttpURLConnection connection = null;
            try {
                URL directionUrl = new URL(getUrl(source, destination));
                connection = (HttpURLConnection) directionUrl.openConnection();
                connection.connect();
                inputStream = connection.getInputStream();

                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                StringBuffer stringBuffer = new StringBuffer();

                String line = "";
                while ((line = bufferedReader.readLine()) != null) {
                    stringBuffer.append(line);
                }

                data = stringBuffer.toString();
                bufferedReader.close();

            } catch (Exception e) {
                Log.e(TAG, "Exception : " + e.toString());
                return null;
            } finally {
                inputStream.close();
                connection.disconnect();
            }
            Log.e(TAG, "Background Task data : " + data);


            JSONObject jsonObject;
            List<List<HashMap<String, String>>> routes = null;

            try {
                jsonObject = new JSONObject(data);
                // Starts parsing data
                DirectionHelper helper = new DirectionHelper();
                routes = helper.parse(jsonObject);
                Log.e(TAG, "Executing Routes : "/*, routes.toString()*/);


                ArrayList<LatLng> points;
                PolylineOptions lineOptions = null;

                // Traversing through all the routes
                for (int i = 0; i < routes.size(); i++) {
                    points = new ArrayList<>();
                    lineOptions = new PolylineOptions();

                    // Fetching i-th route
                    List<HashMap<String, String>> path = routes.get(i);

                    // Fetching all the points in i-th route
                    for (int j = 0; j < path.size(); j++) {
                        HashMap<String, String> point = path.get(j);

                        double lat = Double.parseDouble(point.get("lat"));
                        double lng = Double.parseDouble(point.get("lng"));
                        LatLng position = new LatLng(lat, lng);

                        points.add(position);
                    }

                    // Adding all the points in the route to LineOptions
                    lineOptions.addAll(points);
                    lineOptions.width(10);
                    lineOptions.color(Color.BLUE);

                    Log.e(TAG, "PolylineOptions Decoded");
                }

                // Drawing polyline in the Google Map for the i-th route
                if (lineOptions != null) {
                    return lineOptions;
                } else {
                    return null;
                }

            } catch (Exception e) {
                Log.e(TAG, "Exception in Executing Routes : " + e.toString());
                return null;
            }

        } catch (Exception e) {
            Log.e(TAG, "Background Task Exception : " + e.toString());
            return null;
        }
    }

    @Override
    protected void onPostExecute(PolylineOptions polylineOptions) {
        super.onPostExecute(polylineOptions);
        if (resultCallback != null && polylineOptions != null)
            resultCallback.onPath(polylineOptions);
    }
}

DirectionHelper.java

import com.google.android.gms.maps.model.LatLng;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class DirectionHelper {

    public List<List<HashMap<String, String>>> parse(JSONObject jObject) {

        List<List<HashMap<String, String>>> routes = new ArrayList<>();
        JSONArray jRoutes;
        JSONArray jLegs;
        JSONArray jSteps;

        try {

            jRoutes = jObject.getJSONArray("routes");

            /** Traversing all routes */
            for (int i = 0; i < jRoutes.length(); i++) {
                jLegs = ((JSONObject) jRoutes.get(i)).getJSONArray("legs");
                List path = new ArrayList<>();

                /** Traversing all legs */
                for (int j = 0; j < jLegs.length(); j++) {
                    jSteps = ((JSONObject) jLegs.get(j)).getJSONArray("steps");

                    /** Traversing all steps */
                    for (int k = 0; k < jSteps.length(); k++) {
                        String polyline = "";
                        polyline = (String) ((JSONObject) ((JSONObject) jSteps.get(k)).get("polyline")).get("points");
                        List<LatLng> list = decodePoly(polyline);

                        /** Traversing all points */
                        for (int l = 0; l < list.size(); l++) {
                            HashMap<String, String> hm = new HashMap<>();
                            hm.put("lat", Double.toString((list.get(l)).latitude));
                            hm.put("lng", Double.toString((list.get(l)).longitude));
                            path.add(hm);
                        }
                    }
                    routes.add(path);
                }
            }

        } catch (JSONException e) {
            e.printStackTrace();
        } catch (Exception e) {
        }


        return routes;
    }

    //Method to decode polyline points
    private List<LatLng> decodePoly(String encoded) {

        List<LatLng> poly = new ArrayList<>();
        int index = 0, len = encoded.length();
        int lat = 0, lng = 0;

        while (index < len) {
            int b, shift = 0, result = 0;
            do {
                b = encoded.charAt(index++) - 63;
                result |= (b & 0x1f) << shift;
                shift += 5;
            } while (b >= 0x20);
            int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
            lat += dlat;

            shift = 0;
            result = 0;
            do {
                b = encoded.charAt(index++) - 63;
                result |= (b & 0x1f) << shift;
                shift += 5;
            } while (b >= 0x20);
            int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
            lng += dlng;

            LatLng p = new LatLng((((double) lat / 1E5)),
                    (((double) lng / 1E5)));
            poly.add(p);
        }

        return poly;
    }
}

DirectionPointListener.java

import com.google.android.gms.maps.model.PolylineOptions;

public interface DirectionPointListener {
    public void onPath(PolylineOptions polyLine);
}

Use in Activity or Fragment

LatLng source = new LatLng(xx.xxxx, yy.yyyy);
LatLng destination = new LatLng(xx.xxxx, yy.yyyy);

new GetPathFromLocation(source, destination, new DirectionPointListener() {
            @Override
            public void onPath(PolylineOptions polyLine) {
                yourMap.addPolyline(polyLine);
            }
        }).execute();

For me, I used OSM to get directions as geojson then draw the route on Google map using google maps utils
first

// build.gradle
dependencies {
implementation 'com.google.maps.android:android-maps-utils:0.5'
}

// in maps Activity
// mMap is Google map
// geoJsonData is the returend json object from directions api
// which in my case is OSM
GeoJsonLayer layer = new GeoJsonLayer(mMap, geoJsonData);

// now you can add the layer to the map
layer.addLayerToMap();
// congrats you draw the road between two points now :)

For more information check this Google Maps Android GeoJson Utility.
Happy Coding

You can use this library, and it's simple, check example of usage:

Routing routing = new Routing.Builder()
    .travelMode(AbstractRouting.TravelMode.DRIVING)
    .withListener(this)
    .alternativeRoutes(true)
    .waypoints(start, end)
    .build();
routing.execute();


@Override
public void onRoutingSuccess(List<Route> route, int shortestRouteIndex) {
    progressDialog.dismiss();
    CameraUpdate center = CameraUpdateFactory.newLatLng(start);
    CameraUpdate zoom = CameraUpdateFactory.zoomTo(16);

    map.moveCamera(center);

    if(polylines.size()>0) {
        for (Polyline poly : polylines) {
            poly.remove();
        }
    }

    polylines = new ArrayList<>();
    // Add route(s) to the map.
    for (int i = 0; i <route.size(); i++) {

        //In case of more than 5 alternative routes
        int colorIndex = i % COLORS.length;

        PolylineOptions polyOptions = new PolylineOptions();
        polyOptions.color(getResources().getColor(COLORS[colorIndex]));
        polyOptions.width(10 + i * 3);
        polyOptions.addAll(route.get(i).getPoints());
        Polyline polyline = map.addPolyline(polyOptions);
        polylines.add(polyline);

        Toast.makeText(getApplicationContext(),"Route "+ (i+1) +": distance - "+ route.get(i).getDistanceValue()+": duration - "+ route.get(i).getDurationValue(),Toast.LENGTH_SHORT).show();
    }

    // Start marker
    MarkerOptions options = new MarkerOptions();
    options.position(start);
    options.icon(BitmapDescriptorFactory.fromResource(R.drawable.start_blue));
    map.addMarker(options);

    // End marker
    options = new MarkerOptions();
    options.position(end);
    options.icon(BitmapDescriptorFactory.fromResource(R.drawable.end_green));
    map.addMarker(options);

}

And don't forget to add key using builder from example, if you getting warnings about keyless access (you should have billing account to use it with key)

Kotlin way

implementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation 'com.google.maps.android:android-maps-utils:2.4.0'
implementation 'com.google.maps:google-maps-services:2.1.0'
implementation 'org.slf4j:slf4j-nop:2.0.0'

When implementations are done:

private var mMap: GoogleMap? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val mapFragment = supportFragmentManager.findFragmentById(R.id.frg_map) as SupportMapFragment?
    mapFragment?.let { it ->
        it.getMapAsync { googleMap ->
            mMap = googleMap

            val firstLocation = LatLng( 40.984443, 28.7324437)
            val secondLocation = LatLng(40.9822821, 28.7210424)

            mMap?.addMarker(MarkerOptions().position(firstLocation)
                .icon(bitmapFromVector(this, R.drawable.ic_marker_first)))

            mMap?.addMarker(MarkerOptions().position(secondLocation)
                .icon(bitmapFromVector(this, R.drawable.ic_marker_second)))


            val paths: MutableList<LatLng> = ArrayList()

            val geoApiContext = GeoApiContext.Builder()
                .apiKey(getString(R.string.google_maps_api_key))
                .build()

            val req = DirectionsApi.getDirections(geoApiContext,
                "${secondLocation.latitude},${secondLocation.longitude}",
                "${firstLocation.latitude},${firstLocation.longitude}")
            try {
                val res = req.await()
                if (res.routes.isNullOrEmpty().not()) {
                    val route = res.routes[0]
                    if (route.legs.isNullOrEmpty().not()) {
                        for (leg in route.legs) {
                            if (leg.steps.isNullOrEmpty().not()) {
                                for (step in leg.steps) {
                                    if (step.steps.isNullOrEmpty().not()) {
                                        for (step1 in step.steps) {
                                            step1.polyline?.let { points1 ->
                                                val coordinates = points1.decodePath()
                                                for (coordinate in coordinates) {
                                                    paths.add(LatLng(coordinate.lat, coordinate.lng))
                                                }
                                            }

                                        }
                                    } else {
                                        step.polyline?.let { points ->
                                            val coordinates = points.decodePath()
                                            for (coordinate in coordinates) {
                                                paths.add(LatLng(coordinate.lat, coordinate.lng))
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            } catch (ex: Exception) {
                Log.e("DirectionsApi", "DirectionsApi exception localizedMessage: ${ex.localizedMessage}")
            }

            if (paths.isNotEmpty()) {
                val opts = PolylineOptions().addAll(paths).color(Color.BLUE).width(5f)
                mMap?.addPolyline(opts)
            }

            mMap?.uiSettings?.isZoomControlsEnabled = true

        }
    }
    
    
    
}


private fun bitmapFromVector(context: Context, vectorResId: Int): BitmapDescriptor {
    val vectorDrawable = ContextCompat.getDrawable(context, vectorResId)
    vectorDrawable!!.setBounds(0, 0, vectorDrawable.intrinsicWidth, vectorDrawable.intrinsicHeight)
    val bitmap = Bitmap.createBitmap(vectorDrawable.intrinsicWidth, vectorDrawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    vectorDrawable.draw(canvas)
    return BitmapDescriptorFactory.fromBitmap(bitmap)
}
Related