Build an ASP.NET 2.0 Virtual Earth Map Custom Control

by Peter A. Bromberg, Ph.D.

Peter Bromberg
"Journey all over the universe in a map, without the expense and fatigue of traveling, without suffering the inconveniences of heat, cold, hunger, and thirst." -- Miguel de Cervantes

This little project grew out of a continuing interest in geolocation and mapping (several previous articles here on this site) and also out of convenience - first, kudos to MVP Casey Chesnut for figuring out the algorithm to get the map tiles from the new Virtual Earth servers (you can read about it here -- this guy is amazing!), as well as the fact that I had already put together an ASP.NET Image ServerControl that is capable of displaying images on a web page from memory, not with the requirement for a separate handler to serve the image. So it was "convenient" to simply figure out how to put the pieces together and come up with a nice web control that can be used to automatically display a map based on only a street address.



I should mention at the outset that this is NOT the javascript - driven "panning and zooming" Virtual Earth MapControl, for that one you can find other resources both here and elsewhere on it's use -- instead, this is a control that actually downloads the bitmap tile for the map and creates a hard bitmap for the display. It would be suitable, for example, to display a small map on your blog, or to show the results of an address lookup. You can select the zoom size, the image size for the display, and either aerial or road map display (or hybrid, by selecting both aerial and road together). The control is extremely easy to use - all you provide is a street address and it looks up the coordinates for you. The Virtual Earth server (is it still called that? - - they keep changing names so fast I just can't keep up any longer - it's probably "Windows Live Hamburger" by now) can parse a street address, even without zip code in the form :

1 PARK AVE NEW YORK NY

Without further delay, let's take a look at some code, and then I'll comment afterward:

using System;

using System.Collections.Specialized;

using System.Drawing;

using System.Drawing.Imaging;

using System.IO;

using System.Web;

using System.Web.SessionState;

using System.Web.UI;

using System.Web.UI.Design;

using System.Web.UI.WebControls;

using System.Reflection;

using System.ComponentModel;

using System.Net;

using System.Text;

using System.Text.RegularExpressions;

 

namespace PAB.WebControls

{

 

    public enum Persistence

    {

        Cache,

        Session

    }

 

    [Designer("PAB.WebControls.VEMapControlDesigner"), ToolboxDataAttribute("<{0}:VEMapControl Runat=\"server\"></{0}:VEMapControl>")]

    public class VEMapControl : Control

    {

        protected string ImageUrl;

        private bool ready = false;

 

        public VEMapControl()

        {

        }

 

        public VEMapControl(string address, int size, string ID, string zoomLevel, bool aerial,

            bool road, Persistence mode)

        {

            this.address = address;

            this.size = size;

            this.ID = ID;

            this.zoomLevel = zoomLevel;

            this.aerialMap = aerial;

            this.roadMap = road;         

            this.persistenceType = mode;

            this.GetMap();

        }

 

 

 

        private string address;

        [Description("Address")]

        [Category("Data")]

        [DefaultValue("")]

        [Browsable(true)]

        public string  Address

        {

            get

            {

                return address;

            }

 

            set

            {

                address = value;

            }

        }

 

        private int size;

 

        [Description("Map Size")]

        [Category("Data")]

        [DefaultValue("256")]

        [Browsable(true)]

        public int Size

        {

            get

            {

                return size;

            }

 

            set

            {

                size = value;

            }

        }

 

 

        private string latitude;

        [Description("Latitude")]

        [Category("Data")]

        [DefaultValue("")]

        [Browsable(true)]

        public string Latitude

        {

            get

            {

                return latitude;

            }

 

            set

            {

                latitude = value;

            }

        }

 

        private string longitude;

        [Description("Longitude")]

        [Category("Data")]

        [DefaultValue("")]

        [Browsable(true)]

        public string Longitude

        {

            get

            {

                return longitude;

            }

 

            set

            {

                longitude = value;

            }

        }

 

        private string zoomLevel;

        [Description("Zoom Level")]

        [Category("Data")]

        [DefaultValue("11")]

        [Browsable(true)]

        public string ZoomLevel

        {

            get

            {

                return zoomLevel;

            }

 

            set

            {

                zoomLevel = value;

            }

        }

 

        private bool roadMap;

        [Description("Road Map")]

        [Category("Data")]

        [DefaultValue("true")]

        [Browsable(true)]

        public bool RoadMap

        {

            get

            {

                return roadMap;

            }

 

            set

            {

                roadMap = value;

            }

        }

 

        private bool aerialMap;

        [Description("Aerial Map")]

        [Category("Data")]

        [DefaultValue("false")]

        [Browsable(true)]

        public bool AerialMap

        {

            get

            {

                return aerialMap;

            }

 

            set

            {

                aerialMap = value;

            }

        }

 

        private Persistence persistenceType;

        [Description("Cache or Session Persistence")]

        [Category("Data")]

        [DefaultValue("Cache")]

        [Browsable(true)]

        public Persistence PersistenceType

        {

            get

            {

                return persistenceType;

            }

 

            set

            {

                persistenceType = value;

            }

        }

 

        [Browsable(false)]

        public Bitmap Bitmap

        {

            get

            {

                if (this.PersistenceType == Persistence.Session)

                    return (Bitmap)Context.Session[String.Concat(CreateUniqueIDString(), "Bitmap")];

                else

                    return (Bitmap)Context.Cache[String.Concat(CreateUniqueIDString(), "Bitmap")];

            }

 

            set

            {

                if (this.PersistenceType == Persistence.Session)

          Context.Session[String.Concat(CreateUniqueIDString(), "Bitmap")] = value;

                else

          Context.Cache[String.Concat(CreateUniqueIDString(), "Bitmap")] = value;

            }

        }

 

        private string CreateUniqueIDString()

        {

            string idStr = String.Empty;

            string tmpId = String.Empty;

            if (this.PersistenceType == Persistence.Session)

            {

                idStr = "__" + Context.Session.SessionID.ToString() + "_";

            }

            else

            {

                if (Context.Cache["idStr"] == null)

                {

                    tmpId = Guid.NewGuid().ToString();

                    Context.Cache["idStr"] = tmpId;

                }

                idStr = "__" + Context.Cache["idStr"].ToString() + "_";

            }

 

            idStr = String.Concat(idStr, UniqueID);

            idStr = String.Concat(idStr, "_");

            string pg = Page==null?"pg":Page.ToString();

            idStr = String.Concat(idStr, pg);

            idStr = String.Concat(idStr, "_");

            return idStr;

        }

 

        private void VEMapControl_Init(EventArgs e)

        {

            if (this.address == "" || this.address == null) return;

          double lat1, lat2, long1, long2, midlat, midlong;

        this.SearchForAddress(this.address, out lat1, out long1, out lat2, out long2, out midlat, out midlong);

            this.latitude = midlat.ToString();

            this.longitude = midlong.ToString();

            double lat = double.Parse(this.latitude );

            double lon = double.Parse(this.longitude );

            if (this.zoomLevel == "" || this.zoomLevel==null) this.zoomLevel = "11";

            int zoomLevel = int.Parse(this.zoomLevel );

            int meterY = LatitudeToYAtZoom(lat, zoomLevel);

            int meterX = LongitudeToXAtZoom(lon, zoomLevel);

            int imageSize = 256; 

            int col = meterX / imageSize;

            int row = meterY / imageSize;

            string quadKey = TileToQuadKey(col, row, zoomLevel);

            string url = GetUrl(col, row, quadKey);

            Bitmap b = DownloadImage(url);

            this.Bitmap = b;

            HttpRequest httpRequest = Context.Request;

            HttpResponse httpResponse = Context.Response;

            if (httpRequest.Params[String.Concat("VEMapControl_", UniqueID)] != null)

            {

                httpResponse.Clear();

 

                    httpResponse.ContentType = "Image/Gif";

                    Bitmap.SetResolution(300, 300);

                    Bitmap.Save(httpResponse.OutputStream, ImageFormat.Gif);

 

                httpResponse.End();

            }

            string str = httpRequest.Url.ToString();

            if (str.IndexOf("?") == -1)

            {

                ImageUrl = String.Concat(str, "?VEMapControl_", UniqueID, "=1");

            }

            else

            {

                ImageUrl = String.Concat(str, "&VEMapControl_", UniqueID, "=1");

            }

        }

 

        protected override void OnInit(EventArgs e)

        {

            if(!this.ready )

            VEMapControl_Init(e);

        }

 

        protected override void Render(HtmlTextWriter output)

        {

            ///width={2} height={2} //, size.ToString()

            output.Write("<img id={0} src={1} >", this.UniqueID, ImageUrl );

          //  this.Dispose();

            this.ready = true;

        }

 

        public void GetMap()

        {

            double lat1, lat2, long1, long2, midlat, midlong;

            this.SearchForAddress(this.address, out lat1, out long1, out lat2, out long2, out midlat, out midlong);

 

            this.latitude = midlat.ToString();

            this.longitude = midlong.ToString();

 

            double lat = double.Parse(latitude.Trim());

            double lon = double.Parse(longitude.Trim());

            if (this.zoomLevel == null) this.zoomLevel = "11";

            int zoomLevel = int.Parse(this.zoomLevel);

            int meterY = LatitudeToYAtZoom(lat, zoomLevel);

            int meterX = LongitudeToXAtZoom(lon, zoomLevel);

            if (this.size == 0) this.size = 256;

            int imageSize = this.size;

            int col = meterX / imageSize;

            int row = meterY / imageSize;

            string quadKey = TileToQuadKey(col, row, zoomLevel);

            string url = GetUrl(col, row, quadKey);

            Bitmap b = DownloadImage(url);

            this.Bitmap  = b;

        }

 

 

        public string GetUrl(int tx, int ty, string quadKey)

        {

            string mapType = null;

            string mapExtension = null;

            if (this.roadMap == false && this.aerialMap == false) this.roadMap = true;

            if ( this.roadMap  == true && this.aerialMap  == true)

            {

                mapType = "h";

                mapExtension = ".jpeg";

            }

            else if (this.roadMap  == true)

            {

                mapType = "r";

                mapExtension = ".png";

            }

            else if (this.aerialMap == true)

            {

                mapType = "a";

                mapExtension = ".jpeg";

            }

            else

            {               

                return null;

            }

       

            string url = String.Concat(new object[] { "http://", mapType, quadKey[quadKey.Length - 1], ".ortho.tiles.virtualearth.net/tiles/", mapType, quadKey, mapExtension, "?g=", 1 });

            return url;

        }

 

        public Bitmap DownloadImage(string url)

        {

            int size = this.size;

            Bitmap b = new Bitmap(size,size);

            try

            {

                WebClient wb = new WebClient();

                byte[] baImage = wb.DownloadData(url);

                MemoryStream ms = new MemoryStream(baImage);

                b = (Bitmap)Bitmap.FromStream(ms);

                ms.Close();

            }

            catch (Exception ex)

            {

                throw ex;

            }

            return b;

        }

 

        private const double earthRadius = 6378137;

        private const double earthCircum = earthRadius * 2.0 * Math.PI;

        private const double earthHalfCirc = earthCircum / 2;

 

        private int LatitudeToYAtZoom(double lat, int zoom)

        {

            double arc = earthCircum / ((1 << zoom) * 256);

            double sinLat = Math.Sin(DegToRad(lat));

            double metersY = earthRadius / 2 * Math.Log((1 + sinLat) / (1 - sinLat));

            int y = (int)Math.Round((earthHalfCirc - metersY) / arc);

            return y;

        }

 

        private int LongitudeToXAtZoom(double lon, int zoom)

        {

            double arc = earthCircum / ((1 << zoom) * 256);

            double metersX = earthRadius * DegToRad(lon);

            int x = (int)Math.Round((earthHalfCirc + metersX) / arc);

            return x;

        }

 

        private  double DegToRad(double d)

        {

            return d * Math.PI / 180.0;

        }

 

        private  double MetersPerPixel(int zoom)

        {

            double arc = earthCircum / ((1 << zoom) * 256);

            return arc;

        }

 

        private  string TileToQuadKey(int tx, int ty, int zl)

        {

            string quad = "";

            for (int i = zl; i > 0; i--)

            {

                int mask = 1 << (i - 1);

                int cell = 0;

                if ((tx & mask) != 0)

                {

                    cell++;

                }

                if ((ty & mask) != 0)

                {

                    cell += 2;

                }

                quad += cell;

            }

            return quad;

        }

 

        private  string DoSearchRequest(string searchParams)

        {

            string text1 = string.Empty;

            HttpWebRequest request1 = (HttpWebRequest)WebRequest.Create("http://local.live.com/search.ashx");

            request1.Method = "POST";

            request1.ContentType = "application/x-www-form-urlencoded";

            UTF8Encoding encoding1 = new UTF8Encoding();

            byte[] buffer1 = encoding1.GetBytes(searchParams);

            request1.ContentLength = buffer1.Length;

            try

            {

                Stream stream1 = request1.GetRequestStream();

                stream1.Write(buffer1, 0, buffer1.Length);

                stream1.Close();

                text1 = GetSearchResults(request1);

            }

            catch (WebException)

            {

            }

            return text1;

        }

 

        private  string GetSearchResults(HttpWebRequest searchRequest)

        {

            string text1 = string.Empty;

            HttpWebResponse response1 = (HttpWebResponse)searchRequest.GetResponse();

 

            Stream stream1 = response1.GetResponseStream();

 

            Encoding encoding1 = Encoding.GetEncoding("utf-8");

            StreamReader reader1 = new StreamReader(stream1, encoding1);

            char[] chArray1 = new char[0x100];

            for (int num1 = reader1.Read(chArray1, 0, 0x100); num1 > 0; num1 = reader1.Read(chArray1, 0, 0x100))

            {

                string text2 = new string(chArray1, 0, num1);

                text1 = text1 + text2;

            }

            response1.Close();

            return text1;

        }

 

        public  bool SearchForAddress(string address, out double lat1, out double long1, out double lat2, out double long2, out double midlat, out double midlong)

        {

            double num1;

            double num2;

            double num3;

            double num4 = 0;

            double num5 = 0;

            long2 = num1 = 0;

            long1 = num2 = num1;

            lat2 = num3 = num2;

            lat1 = num3;

            midlat = num4;

            midlong = num5;

            string text1 = "a=&b=" + address + "&c=0.0&d=0.0&e=0.0&f=0.0&g=&i=&r=0";

            string text2 = DoSearchRequest(text1);

            if ((text2 == null) || (text2 == string.Empty))

            {

                return false;

            }

            Regex regex1 = new Regex(@"SetViewport\((?<lat1>\S+),(?<long1>\S+),(?<lat2>\S+),(?<long2>\S+)\)");

            Match match1 = regex1.Match(text2);

            if (!match1.Success)

            {

                return false;

            }

            lat1 = double.Parse(match1.Groups["lat1"].Value);

            long1 = double.Parse(match1.Groups["long1"].Value);

            lat2 = double.Parse(match1.Groups["lat2"].Value);

            long2 = double.Parse(match1.Groups["long2"].Value);

            // we use the midpoint of the viewport here

            midlat = (lat1 + lat2) / 2;

            midlong = (long1 + long2) / 2;

            return true;

        }

    }

 

    public class VEMapControlDesigner : System.Web.UI.Design.ControlDesigner

    {

        public VEMapControlDesigner() { }

        public override string GetDesignTimeHtml()

        {

            return GetEmptyDesignTimeHtml();

        }

 

        protected override string GetEmptyDesignTimeHtml()

        {

            return CreatePlaceHolderDesignTimeHtml("<div>[Image set at runtime. Place control inside Table TD or DIV for positioning.]</div>");

        }

    }

}

There it is. Here is what happens in the control, in a nutshell:

1) The control is instantiated by the Page framework, and all properties are set from the declarative markup of the control tag.

2) The VEMapControl_Init method is called, and this calls the SearchForAddress method to populate the 4 corner coordinates of the ViewPort for the map. The midpoint latitude and longitude of this square are computed to enable to retrieve the correct image tile from the Virtual Earth servers.

3) All the coordinates are computed via Regex matches from the same javascript callback function call that you would get from a regular "javascript" MapControl implementation.

4) The QuadKey of the tile is computed via the formula "corrected" by Casey Chesnut in his work on his original Tile work from his blog at the link provided above.

5) The encoded url to retrieve the exact tile, of the correct type and size, is employed and the webRequest is made.

6) The resulting Bitmap is saved to the Response OutputStream in the correct format, and is cached - either in Session or Cache.

7) the Render method of the control outputs the correct IMG tag which then displays the image.

Sample Hybrid Display from Control: (1835 73rd Ave NE Medina WA 98039)

Do you know whose house that is?

This control still has a few annoying lttle glitches, namely that the resolution must be set to 300 pixels if an aerial map is used (it does this automatically), and a few other minor issues, but it is eminently usable in its current state, so I decided to "Put it in the wild" before I do any more work on it. If you get involved with this and find something or make an enhancement, please be kind enough to post information on the little discussion board at the bottom of this article. As mentioned, this doesn't yet have robust exception handling and there are certainly some enhancements I thought of but haven't put in yet, but -- it works.

We often get this question, so I would like to take this opportunity to remind our readers that all code published on eggheadcafe, while copyrighted to the original author, is published into the public domain with no restrictions on use. NOTE: The "web" folder in this solution is a Web Application Project so if you haven't installed this excellent add-in, you'll need to install it before loading. You could also simply add the web folder as a VS.NET 2005 "Web Site" project if you like, and it would work under the default development web server instead.

Enjoy it!

Download the Visual Studio 2005 solution that acommpanies this article


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: