Monday 11 July 2011

How to build the Compass View

As explained in the previous emails, we now have passed up to date information to the compass view to show us the direction of our place of interest.

We want the compass view to look like this:

Well actually, if you can make it look nicer, please advise!

In this screenshot, we have a north pointing up, but it is normally not. So the whole compass bitmap underneath will be rotated by the corresponding angle.

CompassView is actually a class on its own. It extends View and is merely a canvas to be painted on. We add CompassView to the main layout with the following tag:


<com.metaaps.mobile.compastic.widgets.CompassView
    android:id="@+id/compassview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
/>

The view itself is in fact contained in a ViewFlipper to be able to switch between Compass and Map view, on user request.

Now let's move on to the drawing of the canvas itself.

We have to draw the following layers:
  1. The Magnetic North direction using a bitmap of a compass
  2. The direction of the place of interest from where we are
  3. Additional information which include distance and current position data

In code that gives:

@Override protected void onDraw(Canvas canvas) {

     paint = new Paint();
     
        canvas.drawColor(Color.TRANSPARENT);

        paint.setAntiAlias(true);
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.FILL);

        // draw magnetic north direction
        drawCompass(canvas);
        // draw arrow to show direction to place of interest position
        drawArrow(canvas, currentPlace);
        drawCenter(canvas);
    }


To draw the MN direction we use the following code:

private void drawCompass(Canvas canvas) {
        int w = getWidth();
        int h = getHeight();
        int cx = w / 2;
        int cy = h / 2;
        
        if(canvasBitmap == null) {
         canvasBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.compass);
        }

     canvas.save();
        canvas.translate(cx, cy);
     // rotate to be aligned with the true north
        canvas.rotate(north);
        float maxwidth = (float) (canvasBitmap.getWidth() * Math.sqrt(2));
        float maxheight = (float) (canvasBitmap.getHeight() * Math.sqrt(2));
        float ratio = Math.min(w / maxwidth, h / maxheight);
        int width = (int) (canvasBitmap.getWidth() * ratio);
        int height = (int) (canvasBitmap.getHeight() * ratio);
        // draw the compass
        canvas.drawBitmap(canvasBitmap, new Rect(0, 0, canvasBitmap.getWidth(), canvasBitmap.getHeight()), new Rect(- width / 2, - height/2, width / 2, height / 2), paint);
     canvas.restore();
 }


The idea is to first center and rotate the bitmap by the Magnetic North direction and then paint the compass bitmap. The initial calculation is to make sure the painted bitmap will fit within the canvas dimensions.

Now we move on to the arrow itself. At this stage we are still missing the arrow angle in fact. To calculate the angle, we look at the bearing between the current location and the place of interest. This bearing is actually provided by the Location class itself using the bearingTo method. However the bearing will give you the angle with the true North and not with the Magnetic North. So we need to find out where is the True North first.
For this we need to add the Declination, as explained in an earlier post. The declination value is actually a function of the position and the altitude. The declination values can be found for instance at the NOAA website. I initially loaded all value from there and injected them in a large float table in my application. Declinations vary over time according to a model, so the whole thing meant I also had to update the code every year or so to compensate for the changes. It started to get a bit messy, until I found out about the GeomagneticField class.
This class has a getDeclination method. Initialised with the right lat, long, altitude and time of the day, it will give you the actual declination for any point on earth.
Once you get the declination for your current location you can find out the actual angle by adding all three values: magneticNorth + declination + bearing.

private void drawArrow(Canvas canvas, Place place) {
        int w = getWidth();
        int h = getHeight();
        int cx = w / 2;
        int cy = h / 2;

        if (currentLocation != null && currentPlace != null) {
                // generate the arrow path
         if(arrowPath == null) {
          int height = (int) (Math.min(w / 2, h / 2));
          int width = height / 5;
                        int arrowWidth = 2 * width;
                        int arrowHeight = arrowWidth / 2;
                        arrowPath = new Path();
                        // Construct an arrow path
                        arrowPath.moveTo(- width / 2, height / 3);
                        arrowPath.lineTo(width / 2, height / 3);
                        arrowPath.lineTo(width / 2, - height * 2 / 3);
   arrowPath.lineTo(arrowWidth / 2, - height * 2 / 3);
   arrowPath.lineTo(0, - height * 2 / 3 - arrowHeight);
                        arrowPath.lineTo(- arrowWidth / 2, - height * 2 / 3);
                        arrowPath.lineTo(- width / 2, - height * 2 / 3);
                        arrowPath.lineTo(- width / 2, height / 3);
                        arrowPath.close();
         }
         canvas.save();
                canvas.translate(cx, cy);
         // calculate bearing
         float bearing = currentLocation.bearingTo(place.getLocation());
         // now get the declination angle
         float declination = 0.0f;
      GeomagneticField geoMagneticField = new GeomagneticField((float) currentLocation.getLatitude(), (float) currentLocation.getLongitude(), (float) currentLocation.getAltitude(), new Date().getTime());
      declination = geoMagneticField.getDeclination();
         // calculate the new angle to the true north in degrees
         float MNBearing = (float) (north + declination + bearing);
         // rotate to be aligned with the true north
                canvas.rotate(MNBearing);
                // draw the arrow
                paint.setColor(Color.parseColor("#bdc0dc"));
                paint.setStyle(Style.FILL);
                canvas.drawPath(arrowPath, paint);
                paint.setColor(Color.BLACK);
                paint.setStyle(Style.STROKE);
                paint.setStrokeWidth(2);
                canvas.drawPath(arrowPath, paint);
                // display the distance
         float distance = currentLocation.distanceTo(place.getLocation());
         String distanceStr = "Distance: ";
         if(distance < 10000) {
          distanceStr += (int) distance + " m";
         } else {
          distanceStr += (int) (distance / 1000) + " km";
         }
         canvas.restore();
                // display current location
                Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
                paint.setTextSize(15);
                paint.setColor(Color.DKGRAY);
                paint.setTextAlign(Align.LEFT);
         canvas.drawText(distanceStr, 5, 25, paint);
                paint.setTextSize(15);
         canvas.drawText("Your Loc: (" + ((int) (currentLocation.getLatitude() * 100)) / 100.0 + "º, " + ((int) (currentLocation.getLongitude() * 100)) / 100.0 + "º, " + (int) currentLocation.getAltitude() + "m)", 5, h - 20, paint);
        } else if(currentLocation == null) {
                Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
                paint.setTextSize(15);
                paint.setColor(Color.RED);
                paint.setTextAlign(Align.LEFT);
         canvas.drawText("Current Location is Unknown!", 5, h - 20, paint);
        }
}

And that's it really for the compass view. In the next post I will talk about the Map view and how to get the Geocoder to find a place and its location.

3 comments:

  1. good night, I have some problems with the CurrentLocation, which gives our location is always null, I use the emulator, it affects anything

    ReplyDelete
  2. You need to set the location in the emulator. That's one explanation at http://www.helloandroid.com/tutorials/how-set-location-emulator

    ReplyDelete
  3. Hello, good evening, I use DDMs but beneath always says Unknown Current Location is at the destination location and I'm also with some problems, I have a class that the method getLocation (), one thing I do is the conversion of GeoPoint to location, location I want to point out, I did it, but I know there's something ect. The method getLocation () returns a GeoPoint I do not know if this influences ... thanks

    Greetings

    ReplyDelete