Can I draw a curved dashed line in Google Maps Android?

Solution 1:

You can implement the curved dashed polyline between two points. For this purpose you can use Google Maps Android API Utility Library that has SphericalUtil class and apply some math in your code to create a polyline.

You have to include the utility library in your gradle as

compile 'com.google.maps.android:android-maps-utils:0.5'.

Please have a look at my sample Activity and function showCurvedPolyline (LatLng p1, LatLng p2, double k) that constructs dashed curved polyline between two points. The last parameter k defines curvature of the polyline, it can be >0 and <=1. In my example I used k=0.5

public class MapsActivity extends FragmentActivity implements OnMapReadyCallback {

private GoogleMap mMap;
private LatLng sydney1;
private LatLng sydney2;

@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;

    mMap.getUiSettings().setZoomControlsEnabled(true);

    // Add a marker in Sydney and move the camera
    sydney1 = new LatLng(-33.904438,151.249852);
    sydney2 = new LatLng(-33.905823,151.252422);

    mMap.addMarker(new MarkerOptions().position(sydney1)
            .draggable(false).visible(true).title("Marker in Sydney 1"));
    mMap.addMarker(new MarkerOptions().position(sydney2)
            .draggable(false).visible(true).title("Marker in Sydney 2"));

    mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(sydney1, 16F));

    this.showCurvedPolyline(sydney1,sydney2, 0.5);
}

private void showCurvedPolyline (LatLng p1, LatLng p2, double k) {
    //Calculate distance and heading between two points
    double d = SphericalUtil.computeDistanceBetween(p1,p2);
    double h = SphericalUtil.computeHeading(p1, p2);

    //Midpoint position
    LatLng p = SphericalUtil.computeOffset(p1, d*0.5, h);

    //Apply some mathematics to calculate position of the circle center
    double x = (1-k*k)*d*0.5/(2*k);
    double r = (1+k*k)*d*0.5/(2*k);

    LatLng c = SphericalUtil.computeOffset(p, x, h + 90.0);

    //Polyline options
    PolylineOptions options = new PolylineOptions();
    List<PatternItem> pattern = Arrays.<PatternItem>asList(new Dash(30), new Gap(20));

    //Calculate heading between circle center and two points
    double h1 = SphericalUtil.computeHeading(c, p1);
    double h2 = SphericalUtil.computeHeading(c, p2);

    //Calculate positions of points on circle border and add them to polyline options
    int numpoints = 100;
    double step = (h2 -h1) / numpoints;

    for (int i=0; i < numpoints; i++) {
        LatLng pi = SphericalUtil.computeOffset(c, r, h1 + i * step);
        options.add(pi);
    }

    //Draw polyline
    mMap.addPolyline(options.width(10).color(Color.MAGENTA).geodesic(false).pattern(pattern));
}

}

You can download a sample project with complete code from GitHub

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

Just replace my API key with yours in the app/src/debug/res/values/google_maps_api.xml

enter image description here

Solution 2:

Thanks to @xomena for the great answer. But it has just one little bug. Sometimes, it's arc becoming like a circle. I made a few debugging and see that, we are always using h + 90.0 for heading value at the 12. line of the method. We can solve this by changing that line like this:

LatLng c = SphericalUtil.computeOffset(p, x, h > 40 ? h + 90.0 : h - 90.0);

From now, you probably not encounter this problem again.

Solution 3:

I had the same problem of crooked curved line when I am drawing curve in solid line. After few hours of searching on the internet and trying the different solution. Finally, I came up with the solution (NOT a proper solution but target can be achieved) by using Polygon instead of Polyline. I have modified the above method showCurvedPolyline() to draw a smooth curve and the curve direction will always be in upward direction. Below screenshots are the final result of my modified version.

enter image description here

enter image description here

fun drawCurveOnMap(googleMap: GoogleMap, latLng1: LatLng, latLng2: LatLng) {

    //Adding marker is optional here, you can move out from here.
    googleMap.addMarker(
       MarkerOptions().position(latLng1).icon(BitmapDescriptorFactory.defaultMarker()))
    googleMap.addMarker(
       MarkerOptions().position(latLng2).icon(BitmapDescriptorFactory.defaultMarker()))
    
    val k = 0.5 //curve radius
    var h = SphericalUtil.computeHeading(latLng1, latLng2)
    var d = 0.0
    val p: LatLng?

    //The if..else block is for swapping the heading, offset and distance 
    //to draw curve always in the upward direction
    if (h < 0) {
        d = SphericalUtil.computeDistanceBetween(latLng2, latLng1)
        h = SphericalUtil.computeHeading(latLng2, latLng1)
        //Midpoint position
        p = SphericalUtil.computeOffset(latLng2, d * 0.5, h)
    } else {
        d = SphericalUtil.computeDistanceBetween(latLng1, latLng2)

        //Midpoint position
        p = SphericalUtil.computeOffset(latLng1, d * 0.5, h)
    }

    //Apply some mathematics to calculate position of the circle center
    val x = (1 - k * k) * d * 0.5 / (2 * k)
    val r = (1 + k * k) * d * 0.5 / (2 * k)

    val c = SphericalUtil.computeOffset(p, x, h + 90.0)

    //Calculate heading between circle center and two points
    val h1 = SphericalUtil.computeHeading(c, latLng1)
    val h2 = SphericalUtil.computeHeading(c, latLng2)

    //Calculate positions of points on circle border and add them to polyline options
    val numberOfPoints = 1000 //more numberOfPoints more smooth curve you will get
    val step = (h2 - h1) / numberOfPoints

    //Create PolygonOptions object to draw on map
    val polygon = PolygonOptions()

    //Create a temporary list of LatLng to store the points that's being drawn on map for curve 
    val temp = arrayListOf<LatLng>()

    //iterate the numberOfPoints and add the LatLng to PolygonOptions to draw curve
    //and save in temp list to add again reversely in PolygonOptions
    for (i in 0 until numberOfPoints) {
        val latlng = SphericalUtil.computeOffset(c, r, h1 + i * step)
        polygon.add(latlng) //Adding in PolygonOptions
        temp.add(latlng)    //Storing in temp list to add again in reverse order
    }

    //iterate the temp list in reverse order and add in PolygonOptions
    for (i in (temp.size - 1) downTo 1) {
        polygon.add(temp[i])
    }

    polygon.strokeColor(Color.BLUE)
    polygon.strokeWidth(12f)
    polygon.strokePattern(listOf(Dash(30f), Gap(50f))) //Skip if you want solid line
    googleMap.addPolygon(polygon)

    temp.clear() //clear the temp list
}

Why are we adding temp list again in reverse order in PolygonOptions?

If we do not add LatLng again in PolygonOptions in reverse order, the googleMap.addPolygon() will close the path and the final result will be look like below.

enter image description here

TIPS:

If you want the curve is more in circular shape, increase the value of k. like k = 0.75

Solution 4:

Thanks @xomena for the solution above. It works beautifully in most cases. But there needs some improvement:

  • When k == 1, x will be 0 and midpoint (p) will be the same as mid curve point (c). That means it should be a straight line, but then when you calculate the step, it's not Zero so the final result is a half-circle curve, which is ambiguous with the above condition.

  • When the curve is long enough, let say LIMIT = 1000km, each calculation in h1 + i * step inside the loop make a tiny error to the correct value (due to java double calculation error I guess). Then the start and end points of the polyline not exactly match with start and end coordinations. Moreover, the curvature of the polyline is unpredictable, base on my research, the reason can be the curvature of the Earth's surface that can make your calculation base on heading not correct.

My quick fix is to reset the step to 0 if k == 1 to make it a straight line. For the second problem, if the distance between 2 points is greater than a LIMIT of 1000km, drawing a straight line with k = 1 will be a safer choice to me.