Logo Icon Logo
A Crowd-sourced Cookbook on Writing Great Android® Apps
GitHub logo Twitter logo OReilly Book Cover Art

Draw overlay icon without using Drawable

Author: Keith Mendoza -- Published? false -- FormatLanguage: W

Problem:

How can you display an map overlay in MapView without using Drawable objects?

Solution:

Override the ItemizedOverlay::draw() function.

Discussion:

Warning The Google Maps V1 API is deprecated; new code should use the V2 API discussed in Recipes 4285 and 4297.

This assumes that you have at least done the "Hello, MapView" tutorial, so I will not cover what abstract functions that you need to implement from ItemizedOverlay. The complete source code for Nearby Metars 01.01.0.2 is available for download so some the complete code for the classes mentioned will not be shown in full.

Contents


Overview

Nearby Metars displays the cloud condition icon and the direction part of a wind barb as an overlay on a MapView. This icon is drawn in a way where the cloud condition covers the scale equivalent of about 1 mile around the airport. For anyone curious here is the description of METAR taken from the METARs help page provided by NOAA's Aviation Weather Services:

<para>Weather stations all over the world report weather conditions every hour using a data format referred to as METAR (this is a French acronym with a loose English translation to "routine aviation weather observation"). These data are collected centrally by the U.S. National Weather Service (and other country's equivalents) and distributed.</para>

Page 4 of the help page shows the cloud coverage icons. These are the icons that needs to be drawn as an overlay over the airport to depict the cloud coverage. The wind barb points the wind direction (it's actually the direction the wind is coming from).

Overriding the ItemizedOverlay::draw() function

ItemizedOverlay::draw() is called whenever the MapView needs to be redrawn for whatever reason. Here is the function signature of the draw() function:

public void draw(android.graphics.Canvas canvas,
                 MapView mapView,
                 boolean shadow)

Here are the parameter description taken directly from the API document:

  • canvas - The Canvas upon which to draw. Note that this may already have a transformation applied, so be sure to leave it the way you found it.
  • mapView - the MapView that requested the draw. Use MapView.getProjection() to convert between on-screen pixels and latitude/longitude pairs.
  • shadow - If true, draw the shadow layer. If false, draw the overlay contents.

For each time that the screen is being redrawn the draw() function will be called twice: Once when shadow is true, and again when shadow is false. For Nearby Metars there is no need to draw shadows in overlay items.

For Nearby Metars, MetarList is the MetarItem specific (note to editor: not sure if this is the correct term) implementation of ItemizedOverlay. This class overrides the abstract functions, and the draw() function. This is the code for MetarList::draw():

public void draw(android.graphics.Canvas canvas, MapView mapView, boolean shadow) {
     if(!shadow) {
     Log.v("NearbyMetars", "Drawing items");
     MetarItem item;
     for(int i=0; i<mOverlays.size(); i++) {
          item = mOverlays.get(i);
          item.draw(canvas, mapView);
          }
     }
}

mOverlays is an instance of ArrayList<MetarItem>. Whenever draw() is called, we iterate through mOverlays and call MetarItem::draw(). This implementation makes MetarList and MetarItem tightly coupled for the sake of performance.

Overview of MetarItem class

This class is a subclass of OverlayItem. The mTitle and mSnippet fields inherited from OverlayItem are used for the ICAO code and the raw metar string respectively. There are two fields added in MetarItem:

  • skyCond - This is an instance of the SkyConds enumerated type defined inside MetarItem
  • windDir - This is a float value to store the wind direction

MetarItem::draw() function

This is where the real work of drawing the icon onto the canvas really happens. In the METAR charts from ADDS, the cloud condition icons are drawn using the colors to depict the flight category in effect for that airport; however, as of version 01.01.0.2 Nearby Metars doesn't depict the flight category so the icons are all black. To make thing short the code is broken into sections and the explanation follows after each code snippet.

public void draw(Canvas canvas, MapView mapView) {

This function takes two parameters: canvas and mapView. These two parameters have the same types as the first 2 parameters of ItemizedOverlay::draw().

    //Get the bounds of the icon
    Point point = new Point();
    Projection projection = mapView.getProjection();
    projection.toPixels(mPoint, point);

First we convert the lat,long coordinates of the airport to x,y coordinate. Projection::toPixels() takes a GeoPoint object that stores the lat,long of the location that will be marked by the overlay as the first parameter; and a Point instance to store the x,y coordinate of that location in the MapView canvas.

    final float project = (float)((projection.metersToEquatorPixels((float)1609.344) > 10) ? projection.metersToEquatorPixels((float)1609.344) : 10.0);
    Log.d("NearbyMetars", "Value of project: " + Float.toString(project));
    final RectF drawPos = new RectF(point.x-project, point.y-project, point.x+project, point.y+project);

We then calculate how many pixels 1 mile would be given the map's current zoom level. Then we calculate the bounding coordinates of the icon to be drawn in as a RectF instance.

    //Get the paint to use for drawing the icons
    Paint paint = new Paint();
    paint.setStyle(Paint.Style.STROKE);
    paint.setARGB(179, 0, 0, 0);
    paint.setStrokeWidth(2.0f);
    paint.setStrokeCap(Paint.Cap.BUTT);

A Paint object is instantiated and set to draw a 2 pixel thick black line at about 70% transparency. The reason to not make the cloud condition icons not drawn completely opaque is to allow the user to be able to read the labels on the map. Remember, the cloud icons are drawn on top of the map in a layered fashion.

    switch(skyCond) {
        case CLR:
            canvas.drawRect(drawPos, paint);
            break;
        case SKC:
            canvas.drawCircle(point.x, point.y, project, paint);	
            break;
        case FEW:
            canvas.drawCircle(point.x, point.y, project, paint);
            canvas.drawLine(point.x, drawPos.top, point.x, drawPos.bottom, paint);
            break;
        case SCT:
            canvas.drawArc(drawPos, 0, 270, false, paint);
            paint.setStyle(Paint.Style.FILL_AND_STROKE);
            canvas.drawArc(drawPos, 270, 90, true, paint);
            break;
        case BKN:
            canvas.drawArc(drawPos, 180, 90, false, paint);
            paint.setStyle(Paint.Style.FILL_AND_STROKE);
            canvas.drawArc(drawPos, 270, 270, true, paint);
            break;
        case OVC:
            paint.setStyle(Paint.Style.FILL_AND_STROKE);
            canvas.drawCircle(point.x, point.y, project, paint);
            break;
        case OVX:
            canvas.drawArc(drawPos, 45, 180, true, paint);
            canvas.drawArc(drawPos, 135, 180, true, paint);
            canvas.drawArc(drawPos, 315, 90, true, paint);
            break;
    }

This section of code renders the cloud condition icons based on the value of skyCond. Please see the Canvas reference for the description of the draw*() functions. Drawing the icons for CLR and SKC are straight forward, call the appropriate draw*() function. FEW calls a drawCircle() to draw the circular outline, and then calls the drawLine() to draw the vertical line. In the case of this icon, it won't matter if drawLine() was called first instead of drawCircle(). However, it would be good to remember that successive calls to the draw*() function over the same area will draw shapes on top of each other.

Conditions like SKT, BKN, and OVC first calls drawArc() to draw the unfilled portion of the icon, and then switches the pen style to FILL_AND_STROKE then calls drawArc() again to complete the circle with the filled portion of the icon. The use of drawArc() on these icons are actually an optimization. Canvas::drawCircle() actually calls Canvas::drawArc() under the hood. Why render a graphic that will simply be covered by another graphic drawn in the same location.

    //Draw the wind bar if wind is NOT variable
    if(windDir > 0)
    {
        final float barLen = project * 3;

        //This has been modified to go the opposite direction of
        //standard polar to Cartesian plotting
        canvas.drawLine(point.x, point.y, (float)(point.x + barLen * Math.sin(windDir)), (float)(point.y - barLen * Math.cos(windDir)), paint);
    }
}

This last portion of code draws the wind barb without the wind speed lines. As the comment states, this function calculates the cartesian coordinate with the angle going in a clockwise direction since that's how compass directions go. The standard mathematic polar coordinates have angles going in a counter-clockwise direction. Another thing to note is that the value of project is actually the radian equivalent of the wind compass direction.

Final Thoughts

Using the Canvas::draw*() functions is not necessarily the best method for drawing the overlay icons. Android can render Drawable Resources in a more optimized manner than calling the Canvas::draw*() functions; and it's easier to create great looking images using an image editor. If the overlays for Nearby Metars were done using Drawable Resources, editing the XML files would be cumbersome; using bitmaps will just be a resource hog. Whether to use Drawable or programmatically draw the overlay icon will depend largely on the project's requirements.

See Also:

Hello, MapView Tutorial Canvas class reference ItemizedOverlay class reference OverlayItem class reference Google Add-On API Reference

Download:

The source code for this project can be downloaded from https://github.com/keithmendozasr/NearbyMetars/zipball/01.01.0.2.
keithmendoza 2011-04-24 18:52:24.304 I still need to put in more text in the description. Hopefully, I can get them all in the next day or 2.