Wednesday, 13 July 2011

How to draw a great circle on a Map in Android

What I need: draw a great circle between two points on a Map.

Again in a web application using the Google Maps API this would be a no brainer, as polylines can be directly added to the map and include a great circle option. However this is unfortunately not the case with Android. Actually even drawing a straight line is more complicated. Let's start with a simple line then.

To display a line on the maps we will have to extend the Overlay Class and override the draw method. Drawing a line is quite trivial when you have your canvas. The issue here is to find out which coordinates to use for your points. Remember your points are expressed as Lat, Long coordinates, in degrees x 1E6 actually in the GeoPoint Map API. You will need to convert that to the map coordinate. Thankfully there are a couple of methods to help you achieve that goal in the MapView object.
Once you have you canvas coordinate points it is a pretty trivial task as shown in the small code below:

public class LineOverlay extends Overlay {

    private GeoPoint startGeoPoint;
    private GeoPoint stopGeoPoint;
    
    public LineOverlay(Context context, Location startLocation, Location stopLocation) {
     this.startGeoPoint = new GeoPoint((int) (startLocation.getLatitude() * 1E6), (int) (startLocation.getLongitude() * 1E6));
     this.stopGeoPoint = new GeoPoint((int) (stopLocation.getLatitude() * 1E6), (int) (stopLocation.getLongitude() * 1E6));
 }

 @Override
    public boolean draw(Canvas canvas, MapView mapView, boolean shadow, long when) 
    {
        super.draw(canvas, mapView, shadow);                   

        //---translate the GeoPoint to screen pixels---
        Point startScreenPts = new Point();
        mapView.getProjection().toPixels(this.startGeoPoint, startScreenPts);
        Point stopScreenPts = new Point();
        mapView.getProjection().toPixels(this.stopGeoPoint, stopScreenPts);
        
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(3);
        paint.setStrokeCap(Paint.Cap.ROUND);
        canvas.drawLine(startScreenPts.x, startScreenPts.y, stopScreenPts.x, stopScreenPts.y, paint);         
        return true;
    }

}

If I use the code above this is the line I get:



Which is not bad but not very correct from a geographical point of view. Enters the Great Circle and some calculations algorithm.

@Override
    public boolean draw(Canvas canvas, MapView mapView, boolean shadow, long when) 
    {
        super.draw(canvas, mapView, shadow);                   

        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(3);
        paint.setStrokeCap(Paint.Cap.ROUND);
        // get geodetic points
        List<GeoPoint> points = getGeodeticPoints(startGeoPoint, stopGeoPoint, 20);
        Point previousPoint = null;
        for(GeoPoint point : points) {
         // convert points to map coordinates
            Point mapPoint = new Point();
            mapView.getProjection().toPixels(point, mapPoint);
            if(previousPoint != null) {
                canvas.drawLine(previousPoint.x, previousPoint.y, mapPoint.x, mapPoint.y, paint);         
            }
            previousPoint = mapPoint;
        }

        return true;
    }

 static public List<GeoPoint> getGeodeticPoints(GeoPoint p1, GeoPoint p2, int numberpoints)
    {
                // adapted from page http://maps.forum.nu/gm_flight_path.html
  ArrayList<GeoPoint> fPoints = new ArrayList<GeoPoint>();
  // convert to radians
  double lat1 = (double) p1.getLatitudeE6() / 1E6 * Math.PI / 180;
  double lon1 = (double) p1.getLongitudeE6() / 1E6 * Math.PI / 180;
  double lat2 = (double) p2.getLatitudeE6() / 1E6 * Math.PI / 180;
  double lon2 = (double) p2.getLongitudeE6() / 1E6 * Math.PI / 180;

  double d = 2*Math.asin(Math.sqrt( Math.pow((Math.sin((lat1-lat2)/2)),2) + Math.cos(lat1)*Math.cos(lat2)*Math.pow((Math.sin((lon1-lon2)/2)),2)));
  double bearing = Math.atan2(Math.sin(lon1-lon2)*Math.cos(lat2), Math.cos(lat1)*Math.sin(lat2)-Math.sin(lat1)*Math.cos(lat2)*Math.cos(lon1-lon2))  / -(Math.PI/180);
  bearing = bearing < 0 ? 360 + bearing : bearing;

  for (int n = 0 ; n < numberpoints + 1; n++ ) {
   double f = (1.0/numberpoints) * n;
   double A = Math.sin((1-f)*d)/Math.sin(d);
   double B = Math.sin(f*d)/Math.sin(d);
   double x = A*Math.cos(lat1)*Math.cos(lon1) +  B*Math.cos(lat2)*Math.cos(lon2);
   double y = A*Math.cos(lat1)*Math.sin(lon1) +  B*Math.cos(lat2)*Math.sin(lon2);
   double z = A*Math.sin(lat1)           +  B*Math.sin(lat2);

   double latN = Math.atan2(z,Math.sqrt(Math.pow(x,2)+Math.pow(y,2)));
   double lonN = Math.atan2(y,x);
   fPoints.add(new GeoPoint((int) (latN/(Math.PI/180) * 1E6), (int) (lonN/(Math.PI/180) * 1E6)));
  }

        return fPoints;
 }

}



With the above code the line is now a great circle!


The great circle calculation algorithm was adapted to Java and Android from the http://maps.forum.nu/gm_flight_path.html blog page.

In the next posts I will touch the geocoder and the custom icon display side of the application.

3 comments:

  1. Hi i'd like to add your great circle code to an OSMDroid project - maybe also contribute the new method to the OSMDroid project.

    Your Compastic source is licensed under MIT but i cannot see that you have used the getGeodeticPoints method in Compastic project.

    Can i ask permission to use and modifiy your getGeodeticPoints method in my projects?

    I can let you see my modifications - that's no problem.

    Thanks a lot.

    Martin.

    ReplyDelete
  2. Thanks.

    The new method has now been added to the OSMDroid project:

    http://code.google.com/p/osmdroid/issues/detail?id=338&can=1&q=great%20circle

    Martin.

    ReplyDelete