Showing posts with label Tiled Map Client. Show all posts
Showing posts with label Tiled Map Client. Show all posts

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.

Tile Client Update: New Features and Fixes

As a result of my work on GL Maps, I made several fixes, feature enhancements, and extensions to the original Tile Client and the sample code. Pictures and captions for your viewing pleasure (download is at very bottom):

Composite

I updated the desktop sample to demonstrate how to use a MapOverlay to put pushpins on the map. It also demonstrates how to use CompositeMapSession to blend multiple sessions together. Such as:

  • Satellites + Roads
  • Streets + Traffic

Directions

The desktop sample has also been updated to show how to retrieve and use directions from either Google or Virtual Earth. 

TileRefresh

The samples also demonstrate how to provide a custom refresh bitmap for when the tile is being downloaded.

SmartTile SmartTile2

But, refresh bitmaps are tacky! So, the Tile client now uses parent and child tiles (tiles from other zoom levels) to render temporarily if the tile is missing! On the left you see can see the map using parent tiles to render while the tiles are being downloaded. On the right you see the map again, in full detail, when the tiles have completed downloading.

cftransparent

And a much requested feature: Tiles and overlays now support transparencies on Windows Mobile! Above you can see a MapOverlay that is using alpha blending.

 

Change Log:

  • Developers must now instantiate a GraphicsRenderer and use that to render the map, rather than passing a Graphics object directly. This change was made to allow abstraction of the rendering device to support the 3D rendering a la GL Maps.
  • Tiles are now cached to disk. They will be refreshed from server every 2 days.
  • Fixed Google Directions retrieval.
  • Implemented "smart" tile rendering. If a tile is being downloaded, the parent or child tiles are used to render its content.
  • Fixed bug where MapOverlays were not being affected by the Offset property.
  • Implemented transparencies on Windows Mobile. To retrieve and use a transparent bitmap, simply call GraphicsRenderer.LoadBitmap! See the Windows Mobile sample for more detail.
  • Windows Mobile now converts bitmaps to 16bpp automatically after retrieval. This is a huge performance improvement, because instead of doing the pixel format conversion per paint, it happens only once.
  • Updated both samples to demonstrate more features.
  • Added a TiledMapSession.ClearAgedTiles(N) method to clear the tile cache of tiles that haven't been used in N milliseconds. This is useful for Windows Mobile where memory is a constraint, but clearing all tiles is undesirable.

Full source to the Tile Client and sample applications.

Virtual Earth and Google Maps Tile Server Client for .NET (and Compact Framework)

If you've ever used Live Search, you probably agree that it is hands down the best local search program available for Windows Mobile. It got me curious as to how the maps are rendered so smoothly. A quick investigation made me stumble upon Via Virtual Earth, which explained how to roll a tile server and a corresponding tile client.

I ended up coding a Tile Server client in C# that is compatible with Virtual Earth and Google Maps. This includes the satellite, terrain, traffic, and other such tiles. Here are the grand results:

tiledmaps

On the left, you see a simple client running Virtual Earth on Windows Mobile. Top right, you see the client running on Windows. Below that, you see that same program running Google Maps. Both clients are also displaying the directions from where I live (Seattle) to where I work (Bellevue).

These two programs are just test harnesses, and don't even demonstrate all of capabilities of the tile client. Features include:


    • Blending multiple tile sources (such as maps + traffic, or satellite + traffic)
    • Push pin images
    • Retrieval of directions from both Virtual Earth and Google Maps: the text and an array of geocodes that is used by the client to draw the path
    • Cell Tower geocoding

Sadly I can't release the fully featured map controls that I created that harness all the features, because that would infringe on company IP. However, I can release the full source to the tile and directions clients.

The client makes it trivial to draw a smooth scrolling map. You just instantiate it and tell it to draw on a Graphics object. From there, you can pan, zoom, etc. The client does everything asynchronously: it calls a handler you provide whenever a new tile has completed downloading. The following is the entirety of the Windows Mobile client test harness code:




VirtualEarthMapSession mySession = new VirtualEarthMapSession();
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
mySession.DrawMap(e.Graphics, 0, 0, ClientSize.Width, ClientSize.Height, (o) =>
{
Invoke(new EventHandler((sender, args) =>
{
Invalidate();
}));
}, null);
}

Point myLastPos = Point.Empty;
protected override void OnMouseMove(MouseEventArgs e)
{
mySession.Pan(MousePosition.X - myLastPos.X, MousePosition.Y - myLastPos.Y);
myLastPos = MousePosition;
Invalidate();
base.OnMouseMove(e);
}

protected override void OnMouseDown(MouseEventArgs e)
{
myLastPos = MousePosition;
}

private void myZoomInMenuItem_Click(object sender, EventArgs e)
{
mySession.ZoomIn();
Invalidate();
}

private void myZoomOutMenuItem_Click(object sender, EventArgs e)
{
mySession.ZoomOut();
Invalidate();
}



Note:

This code is really old. I had to update a few of the URLs to the tile servers prior to publishing the code so that it would work. Google tends to change their URL schema relatively often, usually its just a version number in the query string or something. In addition, it looks like the format of the HTML that Google returns for text directions is no longer the same, and consequently client fails when parsing it now. An enterprising developer other than myself would need to figure out how to parse the new HTML format and update the code. However, the rest of it seems to be working (including Virtual Earth directions).

Additionally, I did not create clients for the geocoding services provided by Google Maps nor Virtual Earth, seeing as though my former company had their own. That shouldn't be too difficult, Google has a free REST based geocoding service, and so does Yahoo, MapQuest, and others I believe.

Also, I would have done all the downloading using the anonymous methods technique I described below, but anonymous methods were not available yet. :(