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. :(

49 comments:

Dennis said...

That some excellent piece of work. I've downloaded and used your code in a project of mine. However, I'd like to inform you that there is a bug in the TiledMapSession.Pan method. You should replace all the if's and else if's with while and the code will work better with multiple zoom-and-pan's.

Keep up the good work!

Koush said...

You're right! Thanks for the heads up.

Ivan said...

Thank you very much! It's very helpfull for beginners like me.

Mike said...

Great stuff, thanks! I can't get it to work when I run the mobile version, either with the emulator or on my HTC Mogul. The screen is solid gray, same when zooming in or out. I'll try to track down the problem, but thought I'd check here first in case it's a known issue.

Thanks!
Mike

Dennis said...

Running this code once more after vacation shows that the google maps code now returns a HTTP/403 (permission denied) response. I suppose google will keep changing their access method, so perhaps it'd be a good idea to have the access method/url in a config file. Has anyone investigated this further?

Dennis said...

Correcting myself, this is a protection mechanism installed by google. I get stucked with the same 403 message when accessing http://mt0.google.com/mt?n=404&v=w2.75&x=0&y=0&zoom=100 in my web browser. Is this a general problem or is it just me being blocked?

Dennis said...

I found a solution to my problem above. Modify the format string in GoogleMapSession.GetUriForKey method to
"http://mt.google.com/mt?w=2.75&x={0}&y={1}&zoom={2}" and remove everything related to myCurrentTileServer.

Koush said...

I would not use that solution Dennis: by splitting your request across multiple tile servers, you can download 8 tiles at once, instead of 2. The HTTP protocol limits you to downloading only two concurrent items per server.

The actual fix is to change the URL to this format:

http://mt2.google.com/mt/v=w2.80&hl=en&x=10524&s=&y=22883&zoom=1&s=Galileo

Notices that the URL scheme changed again slightly.

Koush said...

The URL seems to be clipped in the previous post. Maybe inserting spaces will help.

http://mt2.google.com/mt/ v=w2.80&hl=en& x=10524&s=&y=22883&zoom=1

Dennis said...

Thanks a million. May I ask where you find information about the different arguments to use in the URL? (i.e. valid values for v, hl and s)

Koush said...

Download Internet Explorer Developer toolbar. Turn on "view image URLs" and go to maps.google.com.

The URLs the map control uses to show the images will be visible.

Daniel said...

Hi Koush! Congratulations for your code! I'm trying it in a Windows Mobile and it works very well but I would like to get lng and lat from the selected screen pixel but it's not working. What I want is the user could creates a marker selecting the screen.

I'm using YToLatitudeAtZoom and XToLongitudeAtZoom but not always return the correct data. First of all I change this functions adding *256 like this:

static double YToLatitudeAtZoom(int y, int zoom)
{
double arc = EARTH_CIRCUM / ((1 << zoom)*256 );
double metersY = EARTH_HALF_CIRC - (y * arc);
double a = Math.Exp(metersY * 2 / EARTH_RADIUS);
double result = RadToDeg(Math.Asin((a - 1) / (a + 1)));
return result;
}

static double XToLongitudeAtZoom(int x, int zoom)
{
double arc = EARTH_CIRCUM / ((1 << zoom) *256);
double metersX = (x * arc) - EARTH_HALF_CIRC;
double result = RadToDeg(metersX / EARTH_RADIUS);
return result;
}
And I'm obtaining the X and Y pixel like this:

int zz = mySession.Zoom;
int yy = mySession.CenterOffset.Y - (base.Height) / 2 + MousePosition.Y -(Screen.PrimaryScreen.Bounds.Height - base.Height) / 2;
int xx = mySession.CenterOffset.X - base.Width / 2 + MousePosition.X ;

And it works with 0 and 1 zoom level but not with biggest zoom levels.

Any Idea?! Thanks!!! ;)

Daniel

Daniel said...

Koush and Dennis,

Talking about the problem Dennis posted Agoust 28th I may say I think the problem is the Google map version. If you obviate the &v parameter you will get the last map version and you not will have to modify you link every time google updates your map code. Then, you may use this:

uritosend = string.Format("http://mt{0}.google.com/mt?n=404&v=&x={1}&y={2}&zoom={3}", (myCurrentTileServer++) % 4, key.X, key.Y, 17 - key.Zoom);

Koush said...

There is a function that gives you a map center relative X,Y from a Geocode. GeocodeToCenterRelativePoint.

So what you want is a CenterRelativePointToGeocode.

I would undo your changes, as you introduced a couple bugs: X and Y in the "map" dimensions for those functions are not what you think they are. A map at zoom 8 is 65536 pixels width and height.

I also found a bug in my code. Anyways, I have updated my code with that function just now, as well as a couple bug fixes. Please redownload the ZIP.

Here's an example of how to get the Geocode of a location that was clicked:
protected override void OnClick(EventArgs e)
{
base.OnClick(e);
Point p = MousePosition;
p = PointToClient(p);
Point centerRelativePoint = new Point();
centerRelativePoint.X = p.X - ClientSize.Width / 2;
centerRelativePoint.Y = p.Y - ClientSize.Height / 2;
Geocode g = mySession.CenterRelativePointToGeocode(centerRelativePoint);
}

Once you have the Geocode, you can use the TiledMapSession.Overlays to add images/markers to the map.

Daniel said...

Thanks for your fast answer! but the zip file is corrupt, please try uploaded again ;) thanks!

Koush said...

Zip file works for me; I just downloaded and extracted. I am opening it using WinRAR.

Koush said...

You can mail me at koushik underscore dutta @ yahoo.com.

Daniel said...

I’m trying to create a marker using something like this:

MapOverlay mylocalization = new MapOverlay();
mylocalization.Bitmap = new TileBitmap(Properties.Resources.MyLocalizationIcon);
mylocalization.Geocode = geocodeofthesearch;
ms.Overlays.Add(mylocalization);

Where the resource MyLocalizationIcon is a GIF or PNG file.

And I’m trying to put it transparent but nothing to do…

I see in your code some public virtual bool that make mention to DrawTransparent…

Do you know if it’s possible to add PNG or GIF markers and how?

Thnaks again for your time!

Dennis said...

Daniel,

I'm using transparent png markers like this:

Bitmap bmp = new Bitmap(m_iconWidth, m_iconHeight);
bmp.MakeTransparent(m_transparentColor);
Graphics g = Graphics.FromImage(bmp);
g.DoSomeBackgroundDrawing(...);
g.DrawImage(Image.FromFile(@"C:\example.png"),...);
g.DoSomeMoreDrawing(...);
TileBitmap tile = new TileBitmap(bmp);
return new MapOverlay(tile, position, Point.Empty);

Hope it helps! I'm now contacting Koush via e-mail, he can forward my address to you if you need more specifics.

Daniel said...

Thanks Dennis but I'm using compact framework, developing for Windows Mobile and I do not have MakeTransparent for Bitmaps... If I find a solution I'll post here ;) my email is: daniel at adsandgo dot com

Koush said...

For ease of use, I made the Tile Client API use standard .NET Bitmap objects. And you are correct, they do not support per pixel alpha images on .NET CF.

However, I built in extensibility to allow .NET CF to use the Imaging API to do transparent images on .NET CF:
http://msdn.microsoft.com/en-us/library/ms925314.aspx

To implement transparencies on the .NET CF Tile Client, provide a delegate to the "GetTileBitmapCallback" property. When you implement this delegate, it will be called with a stream to the image that was retrieved from the server. Use the Imaging API to load that stream, and return a ITileBitmap that wraps that IImage. Use your ITileBitmap implementation to draw the IImage in the appropriate location, and you will have transparencies in your tiles.

You can also use your an IIMage/ITileBitmap to do transparencies in your overlays.

Daniel said...

wow! too complex for me.. haha! knowing it could be done, I'll investigate more as you tell me. thanks!

Koush said...

It's not that hard, just implement ITileBitmap using the Imaging API. That's all you need to do to support transparent overlays.

Actually, even easier, if you are looking to just do pixel masking (on/off transparencies), in the constructor of the TileBitmap you use for your overlay, specify an image attribute with the appropriate SetColorKey for your transparent color. You can do that without writing a single line of code, and it will work in .NET CF.

ImageAttributes attr = new ImageAttributes();
attr.SetColorKey(Color.Magenta, Color.Magenta);
TileBitmap tile = new TileBitmap(bmp, attr);

Magenta would be your "clear" color in the bitmap.

I wrote this code with extreme ease of use and flexibility in mind. :)

Daniel said...

Finally :) I was playing with this but I did not know where to put it!! now it's running! ;) thanks again... step by step... !!!

Jeremy said...

Koush, while running your sample project, I get exception "Could not find Directions Steps blob." in the GoogleServices.DecodeSteps method. When I remove the "" from the regex the match is made but then I get an exception "ddr_steps' is an unexpected token. The expected token is '"' or '''.

Can you please assist? Thanks in advance.

Koush said...

Sorry, can't help there. I already stated in my post that Google changed the HTML format of the directions returned, and the code is no longer able to parse it. I will not be maintaining this code that much, but you are welcome to write a new parser yourself. You may also want to try the VirtualEarthServices.GetDirections.

Kevin said...

Koush,

Thank you for posting this, it works really well and I can think of all sorts of different applications I can write with it.

To address the problem with google's directions, the response is no longer valid XML, but that can be fixed with a regular expression.

I haven't tested this a whole lot, but making this change in GoogleServices.DecodeSteps() seemed to work:

Change the regex mentioned in jeremy's comment to remove the quotes around ddr_steps and ddr_steps_0

Then a few lines below, use a Regex.Replace to fix the XML (add quotes around attributes):
XmlDocument doc = new XmlDocument();
String fixedMatch = Regex.Replace(match.ToString(), "=([a-zA-Z0-9_:]+?)([ >])", "=\"$1\"$2");
doc.LoadXml(fixedMatch);

DevLord said...

First of all, Thank you very much for this code!
I have one question though:

Images are flickering when scrolling. Any idea how a can add double buffering to your code to stop this flickering?

Thanx you.

Armando Rocha said...

Thanks for this amazing code!

This code help me a lot. I made some changes and create an
Map Overlay Alpha class to allow put transparent images in Map.

If anyone have interest in that, mail me and i send the code.

A. Rocha (Portugal)

Koush said...

The desktop version already supports alpha through the MapOverlay class. You are correct in that it does not support it in the Windows Mobile version though.

For Windows Mobile I overrode GetTileBitmapCallback to return a ITileBitmap that was implemented using the Imaging API (IImage).

I am almost done preparing a release that will natively support transparencies on Windows Mobile, so other developers don't have to go through the same headache as me.

Armando Rocha said...

Hi Koush,

I know that the desktop version already suport alpha.

But i'm working on compact framework and MapOverlay not support alpha, that's why i create a MapOverlayAlpha class.

Now its working, but im waiting for this new release.

One more question. Its possible draw polylines over the map?

Thanks a lot Koush, for share this code with the begginers developers like me.

Jake Stevenson said...

Thank you very much for this great library. I was not looking forward to trying to write my own :)

I'm having a bit of trouble determining the radius of the area covered in the current zoom level.

I thought I'd calculate the geocode of the center of the left side of the screen calculate the distance to the geocode of the center.

My first issue was that the CenterRelativePointToGeocode() code always returned the same geocode, no matter what point was passed. I modified the routine to the following:

public Geocode CenterRelativePointToGeocode(Point point)
{
Geocode ret = new Geocode();
ret.Longitude = XToLongitudeAtZoom(point.X, myZoom + 8);
ret.Latitude = YToLatitudeAtZoom(point.Y, myZoom + 8);
return ret;
}

This does return a geocode, but I have no idea if it's valid or not. When I feed these to my distance calculation, the distance is too small.

Is this the right way to determine the geocode for a point? Or is there an easier way to determine the area available?

Thanks again.

Qasem said...

Hi Koush :)
thank very much for ur Code
I need to do the reverse and display the Lat/Long of the cursor moving over the map.
im beginnere programmer :)
plz help me

murphy said...

This is some great code. However the function
public Point GeocodeToCenterRelativePoint(Geocode geocode)
{
int centerXReference = myCenterTile.X << 8 + myCenterOffset.X;
int centerYReference = myCenterTile.Y << 8 +myCenterOffset.Y;
int px = LongitudeToXAtZoom(geocode.Longitude, myZoom +8);
int py = LatitudeToYAtZoom(geocode.Latitude, myZoom +8);
return new Point(px - centerXReference, py - centerYReference);
}
doesn't work properly. When myCenterTile.X and Y are 0 it's good since 0 << anything is 0. But if myCenterTile.X or Y are greater than 0 they are left shifted by (8 + myCenterOffset.X). So modifying the first 2 lines to
int centerXReference = myCenterTile.X << 8;
int centerYReference = myCenterTile.Y << 8;
works for me.

Koush said...

Hi Murphy, thanks for pointing out the bug! However, the code you provided is also incorrect. My error in calculation was due to operator ordering. The byte shift was happening after the addition to the offset, which was unintended:

Wrong code:
int centerXReference = myCenterTile.X << 8 + myCenterOffset.X;
int centerYReference = myCenterTile.Y << 8 + myCenterOffset.Y;


Correct code:
int centerXReference = (myCenterTile.X << 8) + myCenterOffset.X;
int centerYReference = (myCenterTile.Y << 8) + myCenterOffset.Y;

I'll update my code with hte fixes.

Koush said...

Hi Jake, you are correct that GeocodeToCenterRelativePoint has a bug in it. The proper fix is:

Geocode ret = new Geocode();
ret.Longitude = XToLongitudeAtZoom((myCenterTile.X << 8) + myCenterOffset.X + point.X, myZoom + 8);
ret.Latitude = YToLatitudeAtZoom((myCenterTile.Y << 8) + myCenterOffset.Y + point.Y, myZoom + 8);


I'll verify this and update the source code.

eastlands said...

Thanks for this excellent post. This has been of great assistance in getting a map tile engine working.

I have a question about use of Google and Virtual Earth data. In the past I have always needed to provide some sort of licence key (for MS MapPoint) so that the map provider could track (and potentially limit) my use of their map tiles.

Is the use of Google Earth and VE Earth tiles unrestricted now?

Anonymous said...

Hi,

i want zoom to marker automaticaly at startup,but if i set zoom parameter to 10 for example the zoom is not on marker.
How can i do?
tnx

acquariusoft said...

HI, in this year i have some problem to download images with big zoom es 13
someone have the same problem?

Bye

Renzo said...

Has anyone checked on the Google Maps License Terms? is this allowed?

Koush said...

You will need an appropriate license to use the services.
There are also free services such as OpenStreetMap.

John said...

Hi,

I am just trying to learn about all this stuff and your code is a great help! Thanks a lot.

When I try to run the code, both the TiledMaps example and the GLMaps example, I just get big gray rectangles instead of the tiled maps.

I guess this is due to the url of the tile server being out of date. My problem is that I have no idea what to update it with. Is there an easy way to find out the new one?

Thanks and keep up your great work!

Scott Rock said...

First of all, thanks for this great code! I'm trying to use the map in a .NET CF application written in VB.net. The map tiles display correctly except for the last refresh. All but the last refresh is getting displayed, leaving the map somewhat "blurry". It is as though the final callback is not getting received.

Here is the code. Do you see anything wrong with it that may be preventing the final callback from happening?



Private Sub RefreshMap()
' clear out tiles that haven't been used in 10 seconds, just to keep from running out of memory.
mapSession.ClearAgedTiles(10000)

If (mapBitmap Is Nothing) Then
mapBitmap = New Bitmap(mapPictureBox.ClientSize.Width, mapPictureBox.ClientSize.Height, System.Drawing.Imaging.PixelFormat.Format16bppRgb565)
mapRenderer.Graphics = Graphics.FromImage(mapBitmap)
mapPictureBox.Image = mapBitmap


End If

mapSession.DrawMap(mapRenderer, 0, 0, mapBitmap.Width, mapBitmap.Height, New WaitCallback(AddressOf RefreshMapCallback), Nothing)

mapPictureBox.Refresh()

End Sub

Private Sub RefreshMapCallback(ByVal State As Object)
RefreshMap()
End Sub

Any ideas/suggestions would be much appreciated!

Antonio said...

Hi! I'm trying to use this example but cannot see the google maps tiles. I think there is a problem with the tiles links?

Manuel said...

Hi Koush! Congratulations for your code! I'm using it for some WM projects. I had to overcome a projection issue because my Tile Server works with the Elliptical Mercator Projection instead of the Spherical one used by Google and Bing. Please find in attachment a class which implements the necessary projection.

using System;

using System.Collections.Generic;
using System.Text;

namespace TiledMaps
{
class MercatorProjection
{
private static readonly double R_MAJOR = 6378137.0;
private static readonly double R_MINOR = 6356752.3142;
private static readonly double RATIO = R_MINOR / R_MAJOR;
private static readonly double ECCENT = Math.Sqrt(1.0 - (RATIO * RATIO));
private static readonly double COM = 0.5 * ECCENT;

private static readonly double DEG2RAD = Math.PI / 180.0;
private static readonly double RAD2Deg = 180.0 / Math.PI;
private static readonly double PI_2 = Math.PI / 2.0;

public double[] toPixel(double lon, double lat)
{
return new double[] { this.lonToX(lon), this.latToY(lat) };
}

public double[] toGeoCoord(double x, double y)
{
return new double[] { this.xToLon(x), this.yToLat(y) };
}

public double lonToX(double lon)
{
double temp = this.DegToRad(lon);
temp *= R_MAJOR;
return temp;
}

public double latToY(double lat)
{
lat = Math.Min(85.05112878, Math.Max(lat, -85.05112878));
double phi = this.DegToRad(lat);
double sinphi = Math.Sin(phi);
double con = ECCENT * sinphi;
con = Math.Pow(((1.0 - con) / (1.0 + con)), COM);
double ts = Math.Tan(0.5 * ((Math.PI * 0.5) - phi)) / con;
double temp = Math.Log(ts);
temp = 0 - temp;
temp = 0.5 - temp / Math.PI / 2;
return temp;
}

public double xToLon(double x)
{
return this.RadToDeg(x) / R_MAJOR;
}

public double yToLat(double y)
{
double ts = Math.Exp(-y * 2 * Math.PI);
double phi = Math.PI / 2 - 2 * Math.Atan(ts);
double dphi = 1.0;
int i = 0;
while ((Math.Abs(dphi) > 0.000000001) && (i < 15))
{
double con = ECCENT * Math.Sin(phi);
dphi = Math.PI / 2 - 2 * Math.Atan(ts * Math.Pow((1.0 - con) / (1.0 + con), COM)) - phi;
phi += dphi;
i++;
}
return this.RadToDeg(phi);
}

private double RadToDeg(double rad)
{
return rad * RAD2Deg;
}

private double DegToRad(double deg)
{
return deg * DEG2RAD;
}
}
}

To use it just call it within the LatitudeToYAtZoom and YToLatitudeAtZoom methods. Cheers Manuel

Koharudin said...

wow, nice work, u saved my time.
btw, how to port it to j2me?

Thanks

Dale said...

Koush, truly impressive code!

I tested the application on my Windows Mobile device, Virtual Earth maps work great, however the Google Maps do not render.

Any ideas why? Could it be a change in the Google Map url format?

Fanis said...

Hello Koush,

Great work. But I have a problem. Sometimes the correct tile is not displayed but instead a irrelevant tile is shown in its place. I'm running the application on a HTC HD device.

Anyone has an idea?

Joshua Smith said...

Thanks for helpful tips. For business solutions it is the right way to get outsourcing software development services.