Drawing Text Overlays using the Tiled Map Client

(and the internal workings of the Tile Rendering Engine)

TextMapDrawable

This morning, I got an email from one of the users of the Tiled Maps library. He pointed out that although it was easy to place Bitmap overlays at a position on the map, he couldn't figure out how to draw text at that position. His approach was to draw text on a Bitmap and then draw that Bitmap onto the map. The problem he was running into, however, was having a proper transparent background on that Bitmap (Windows Mobile does not support an alpha channel). Although it is possible to do a masked blit in Windows Mobile, this method of drawing text onto a map is not ideal.

If one examines the internals of the Tiled Map Client, you will find that overlays that appear on the map actually have a very flexible abstraction layer around them. The TiledMapSession itself has no internal knowledge of the overlay renderering implementation, in that it does not actually perform the drawing. It is only concerned about the Width and Height, so it can perform proper layout to let the rendering engine (IMapRenderer) to draw the content at the proper location. This is how the Tiled Map Client is flexible enough to perform both 2D and 3D rendering:

/// <summary>
/// IMapRenderer provides the methods necessary for a TiledMapSession
/// to draw tiles to compose the map, as well as the other content
/// that may appear on the map.
/// </summary>
public interface IMapRenderer
{
    /// <summary>
    /// Get a IMapDrawable from a stream that contains a bitmap.
    /// </summary>
    /// <param name="session">The map session requesting the bitmap</param>
    /// <param name="stream">The input stream</param>
    /// <returns>The resultant bitmap</returns>
    IMapDrawable GetBitmapFromStream(TiledMapSession session, Stream stream);

    /// <summary>
    /// Given a IMapDrawable, draw its contents.
    /// </summary>
    /// <param name="drawable">The IMapDrawable to be drawn.</param>
    /// <param name="destRect">The destination rectangle of the drawable.</param>
    /// <param name="sourceRect">The source rectangle of the drawable.</param>
    void Draw(IMapDrawable drawable, Rectangle destRect, Rectangle sourceRect);

    /// <summary>
    /// Draw a filled rectangle on the map.
    /// </summary>
    /// <param name="color">The fill color.</param>
    /// <param name="rect">The destination rectangle.</param>
    void FillRectangle(Color color, Rectangle rect);

    /// <summary>
    /// Draw a line strip on the map.
    /// </summary>
    /// <param name="lineWidth">The width of the line stripe.</param>
    /// <param name="color">The line strip color.</param>
    /// <param name="points">The points which compose the line strip.</param>
    void DrawLines(float lineWidth, Color color, Point[] points);
}

/// <summary>
/// IMapDrawable is the interface used by the Tiled Map Client to represent content
/// onto a IMapRenderer.
/// IMapDrawable is generally tied to an implementation of IMapRenderer, 
/// which is responsible for internally representing and rendering the drawable.
/// </summary>
public interface IMapDrawable : IDisposable
{
    /// <summary>
    /// The width of the drawable content.
    /// </summary>
    int Width
    {
        get;
    }

    /// <summary>
    /// The height of the drawable content.
    /// </summary>
    int Height
    {
        get;
    }
}

So, how do we go from drawing a bitmap to drawing text? The standard/normal implementation and usage of an IMapRenderer is the GraphicsRenderer. The GraphicsRenderer is what facilitates rendering of a TiledMapSession to a System.Drawing.Graphics instance. Let's take a look at it's implementation of Draw:

public void Draw(IMapDrawable drawable, Rectangle destRect, Rectangle sourceRect)
{
    IGraphicsDrawable graphicsDrawable = drawable as IGraphicsDrawable;
    graphicsDrawable.Draw(Graphics, destRect, sourceRect);
}

As you can see, it casts the IMapDrawable to an IGraphicsDrawable and calls its implementation of Draw, passing it the Graphics object:

/// <summary>
/// IGraphicsDrawable is a type of IMapDrawable that can draw to a
/// System.Drawing.Graphics instance.
/// </summary>
public interface IGraphicsDrawable : IMapDrawable
{
    void Draw(Graphics graphics, Rectangle destRect, Rectangle sourceRect);
}

There are two provided implementations of IGraphicsDrawable: WinCEImagingBitmap, which uses the Imaging API to draw bitmaps that contain alpha, and StandardBitmap, which draws a standard System.Drawing.Bitmap. So what we need, is a third implementation, which I called TextMapDrawable. TextMapDrawable will implement IGraphicsDrawable and use Graphics.DrawString to draw text onto the Graphics object.

Here's my implementation of TextMapDrawable:

public class TextMapDrawable : IGraphicsDrawable
{
    static Bitmap myMeasureBitmap = new Bitmap(1, 1, PixelFormat.Format16bppRgb565);
    static Graphics myMeasureGraphics = Graphics.FromImage(myMeasureBitmap);

    public float MaxWidth
    {
        get;
        set;
    }

    public float MaxHeight
    {
        get;
        set;
    }

    Brush myBrush;
    public Brush Brush
    {
        get
        {
            return myBrush;
        }
        set
        {
            myBrush = value;
        }
    }

    bool myDirty = true;
    string myText;
    public string Text
    {
        get
        {
            return myText;
        }
        set
        {
            myText = value;
            myDirty = true;
        }
    }

    Font myFont;
    public Font Font
    {
        get
        {
            return myFont;
        }
        set
        {
            myDirty = true;
            myFont = value;
        }
    }

    #region IGraphicsBitmap Members

    public void Draw(Graphics graphics, Rectangle destRect, Rectangle sourceRect)
    {
        // just ignore source rect, doesn't mean anything in this context.
        if (CalculateDimensions() && myBrush != null)
            graphics.DrawString(myText, myFont, myBrush, destRect.X, destRect.Y);
    }

    #endregion

    bool CalculateDimensions()
    {
        bool valid = !string.IsNullOrEmpty(myText) && myFont != null;

        if (myDirty)
        {
            myDirty = false;
            if (valid)
            {
                SizeF size = myMeasureGraphics.MeasureString(myText, myFont);
                myWidth = (int)Math.Ceiling(size.Width);
                myHeight = (int)Math.Ceiling(size.Height);
            }
            else
            {
                myWidth = 0;
                myHeight = 0;
            }
        }
        return valid;
    }

    #region IMapBitmap Members

    int myWidth;
    public int Width
    {
        get
        {
            CalculateDimensions();
            return myWidth;
        }
    }

    int myHeight;
    public int Height
    {
        get
        {
            CalculateDimensions();
            return myHeight;
        }
    }

    #endregion

    #region IDisposable Members

    public void Dispose()
    {
    }

    #endregion
}

As you can see, it took only 38 lines of code (according to Visual Studio's Code Metrics) to allow drawing of text to the map! I have also updated the Tiled Map Client source for those interested in these changes.

1 comments:

Unknown said...

Hi Koush, and thanks for your great job. I'm trying to make a compisite map session with 2 map sessions, the first one is google and the second one with custom tiles. It works good, but unfortunately I want the second map with some alpha blending over google maps. Do you thins it is feasible ?