31

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?

nbro
  • 15,395
  • 32
  • 113
  • 196
Lahiru Chandima
  • 22,324
  • 22
  • 103
  • 179
  • Take a look at [this](https://stackoverflow.com/a/47329553/6950238) answer. And combine it with Directions API. – Andrii Omelchenko Nov 26 '17 at 10:16
  • @AndriiOmelchenko I tried this, but the distance between the step points returned by the directions API can be pretty large, and snap to roads only works when the points given are close to each other. – Lahiru Chandima Nov 26 '17 at 11:48
  • Try request directions not for all path, but step by step with several shortest distances. – Andrii Omelchenko Nov 26 '17 at 14:17
  • @AndriiOmelchenko, If I request directions between the start and end of a single step of the whole route, google doesn't divide that step into smaller steps. So, I can't increase the number of points in the route returned for the whole route this way. – Lahiru Chandima Nov 26 '17 at 16:19
  • Or/and use [waypoints](https://developers.google.com/maps/documentation/directions/intro#Waypoints) in Directions API. – Andrii Omelchenko Nov 26 '17 at 21:09
  • You always can apply Google Maps Roads API to two points of Directions API request result. – Andrii Omelchenko Nov 26 '17 at 21:11
  • @AndriiOmelchenko I don't know any other location which falls on the route, other than the start and end location, so I am unable to use waypoints. Also, as I have mentioned earlier, two points returned by directions API can be too far from each other, and roads API doesn't work well when points are too far. – Lahiru Chandima Nov 27 '17 at 00:56
  • Use "two points returned by directions API" as start and finish for Directions API again, and so on. Then on last step use Google Maps Roads API. – Andrii Omelchenko Nov 27 '17 at 09:37
  • @AndriiOmelchenko as I mentioned earlier, if I call directions API again on "two points returned by directions API", it doesn't divide that two points to any more points. It simply returns the same two points. I cannot get any point in the middle of that two points by this way. – Lahiru Chandima Nov 27 '17 at 15:00
  • Please add start/finish points coords for testing on your data. – Andrii Omelchenko Nov 28 '17 at 15:59

5 Answers5

72

Indeed, you can draw precise route in Google Maps Android API using results provided by Directions API web service. If you read the documentation for Directions API you will see that response contains information about route legs and steps. Each step has a field polyline that is described in the documentation as

polyline contains a single points object that holds an encoded polyline representation of the step. This polyline is an approximate (smoothed) path of the step.

So, the main idea to solve your issue is to get response from Directions API, loop through route legs and steps, for each step get encoded polyline and decode it to the list of coordinates. Once done you will have a list of all coordinates that compound the route, not only begin and end point of each step.

For simplicity I recommend using the Java client library for Google Maps Web services:

https://github.com/googlemaps/google-maps-services-java

Using this library you can avoid implementing your own async tasks and decoding function for polylines. Read the documentation to figure out how to add the client library in your project.

In Gradle it should be something similar to

compile 'com.google.maps:google-maps-services:(insert latest version)'
compile 'org.slf4j:slf4j-nop:1.7.25'

I have created a simple example to demonstrate how it works. Have a look at my comments in the code

public class MapsActivity extends FragmentActivity implements OnMapReadyCallback {

    private GoogleMap mMap;
    private String TAG = "so47492459";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_maps);
        // Obtain the SupportMapFragment and get notified when the map is ready to be used.
        SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
                .findFragmentById(R.id.map);
        mapFragment.getMapAsync(this);
    }

    @Override
    public void onMapReady(GoogleMap googleMap) {
        mMap = googleMap;

        LatLng barcelona = new LatLng(41.385064,2.173403);
        mMap.addMarker(new MarkerOptions().position(barcelona).title("Marker in Barcelona"));

        LatLng madrid = new LatLng(40.416775,-3.70379);
        mMap.addMarker(new MarkerOptions().position(madrid).title("Marker in Madrid"));

        LatLng zaragoza = new LatLng(41.648823,-0.889085);

        //Define list to get all latlng for the route
        List<LatLng> path = new ArrayList();


        //Execute Directions API request
        GeoApiContext context = new GeoApiContext.Builder()
                .apiKey("YOUR_API_KEY")
                .build();
        DirectionsApiRequest req = DirectionsApi.getDirections(context, "41.385064,2.173403", "40.416775,-3.70379");
        try {
            DirectionsResult res = req.await();

            //Loop through legs and steps to get encoded polylines of each step
            if (res.routes != null && res.routes.length > 0) {
                DirectionsRoute route = res.routes[0];

                if (route.legs !=null) {
                    for(int i=0; i<route.legs.length; i++) {
                        DirectionsLeg leg = route.legs[i];
                        if (leg.steps != null) {
                            for (int j=0; j<leg.steps.length;j++){
                                DirectionsStep step = leg.steps[j];
                                if (step.steps != null && step.steps.length >0) {
                                    for (int k=0; k<step.steps.length;k++){
                                        DirectionsStep step1 = step.steps[k];
                                        EncodedPolyline points1 = step1.polyline;
                                        if (points1 != null) {
                                            //Decode polyline and add points to list of route coordinates
                                            List<com.google.maps.model.LatLng> coords1 = points1.decodePath();
                                            for (com.google.maps.model.LatLng coord1 : coords1) {
                                                path.add(new LatLng(coord1.lat, coord1.lng));
                                            }
                                        }
                                    }
                                } else {
                                    EncodedPolyline points = step.polyline;
                                    if (points != null) {
                                        //Decode polyline and add points to list of route coordinates
                                        List<com.google.maps.model.LatLng> coords = points.decodePath();
                                        for (com.google.maps.model.LatLng coord : coords) {
                                            path.add(new LatLng(coord.lat, coord.lng));
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        } catch(Exception ex) {
            Log.e(TAG, ex.getLocalizedMessage());
        }

        //Draw the polyline
        if (path.size() > 0) {
            PolylineOptions opts = new PolylineOptions().addAll(path).color(Color.BLUE).width(5);
            mMap.addPolyline(opts);
        }

        mMap.getUiSettings().setZoomControlsEnabled(true);

        mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(zaragoza, 6));
    }
}

Please note that for web service you have to create a separate API key, the API key with Android app restriction won't work with web service.

The result of my example is shown in screenshot

enter image description here

You can also download a complete sample project from

https://github.com/xomena-so/so47492459

Don't forget replace the API key with yours.

I hope this helps!

xomena
  • 31,125
  • 6
  • 88
  • 117
  • How to draw for multiple point please guid me sir..for example if i have 50 latlng..i want to draw polyline on the road please any suggestion – Gowthaman M Jun 14 '18 at 07:56
  • @xomena I have changed your api key with my api key, and I have activated maps for android and directions api the maps is showed with the markers but the route draw is not showed. I get this error This IP, site or mobile application is not authorized to use this API key.... How can I solve that? Note. with your api key the route is showed – Dimoreno Jul 23 '18 at 17:49
  • 1
    I copy/pasted this code, but it crashed with the latest version of google.maps.services that is 0.9.0 , with this exception : NoClassDefFoundError: Failed resolution of: Ljava/time/LocalDateTime . I switched to googl.maps.services:0.2.11 and it is ok now – Nabzi Nov 20 '18 at 09:17
  • 1
    @Golnar According to the documentation [java.time.LocalDateTime](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html) was introduced in Java 8. I suppose version 0.9.0 requires Java 8 o higher. – xomena Nov 20 '18 at 09:39
  • Important note from the documentation: "This library is not intended for use inside of an Android app, due to the potential for loss of API keys." (https://github.com/googlemaps/google-maps-services-java) – Duncan Lukkenaer Jan 17 '19 at 15:53
  • @Duncan Luk Correct. The optimal solution is create an intermediate server where you can execute Directions requests using the Java client library and pass results back to the Android app. – xomena Jan 17 '19 at 16:07
  • This answer is so good that I adapted it to primefaces gMap and it worked, very nice – BugsForBreakfast Jul 02 '19 at 00:45
  • @Xomena Hey man just one question, im trying to implement the GeolocationApiRequest but it receives a payload argument and I have no idea what is that, can you help me? this is what I have im following your code: GeolocationApiRequest req = (GeolocationApiRequest) GeolocationApi.geolocate(context, payload); – BugsForBreakfast Jul 04 '19 at 04:53
  • @Xomena please checkout my questions the one with title "How to properly use GeolocationApiRequest" I manage to make it work, but the results aren't very accurate (im talking like 30 or more meters innacuraccy but its probably something im doing wrong) I would like you to check if the way im implementing it is right, thank you. – BugsForBreakfast Jul 04 '19 at 17:59
  • How did you make sure that these two points are always inside the screen? @xomena – Rohit Singh Apr 05 '20 at 17:06
  • @xomena how can i update my position of one marker if one marker is a moving one? just like uber? – Manoj Perumarath Jan 08 '22 at 08:31
  • Thanks for detailed answer i got an exception Cannot find local variable 'coord' in catch how to solve this – Ouneeb Ur Rehman Jul 15 '22 at 13:05
6

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();
Ketan Ramani
  • 4,874
  • 37
  • 42
  • Hi, I have an API key for android. But using that I am getting no result. I have set all the restrictions to None. But Google server is sending me Null arrays. I have got this response : E/GetPathFromLocation: Background Task data : { "geocoded_waypoints" : [ {}, {} ], "routes" : [], "status" : "ZERO_RESULTS"} – Yunus Oct 29 '18 at 16:35
  • Caution: Using the via: prefix to avoid stopovers results in directions that are very strict in their interpretation of the waypoint. This may result in severe detours on the route or ZERO_RESULTS in the response status code if the Directions API is unable to create directions through that point. (https://developers.google.com/maps/documentation/directions/intro) Remove via: prefix if you have used in url – Ketan Ramani Nov 03 '18 at 05:20
  • I am in a job technical challenge and this post solved my problem :) Thanks. – Asad Ali Choudhry Oct 25 '19 at 17:13
  • @AsadAliChoudhry Welcome – Ketan Ramani Nov 02 '19 at 07:51
  • Check Advance Version https://stackoverflow.com/a/59622783/6667442 – Ketan Ramani Jan 07 '20 at 05:40
3

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

Raed
  • 844
  • 11
  • 10
2

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)

Max Base
  • 639
  • 1
  • 7
  • 15
B-GangsteR
  • 2,534
  • 22
  • 34
1

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)
}
Emefar
  • 156
  • 1
  • 8