Subversion Repositories Projects

Rev

Go to most recent revision | Blame | Last modification | View Log | RSS feed


namespace GMap.NET.MapProviders
{
   using System;
   using System.Collections.Generic;
   using System.Diagnostics;
   using System.Globalization;
   using System.Net;
   using System.Text;
   using System.Text.RegularExpressions;
   using System.Threading;
   using System.Xml;
   using GMap.NET.Internals;
   using GMap.NET.Projections;

   public abstract class BingMapProviderBase : GMapProvider, RoutingProvider, GeocodingProvider
   {
      public BingMapProviderBase()
      {
         MaxZoom = null;
         RefererUrl = "http://www.bing.com/maps/";
         Copyright = string.Format("©{0} Microsoft Corporation, ©{0} NAVTEQ, ©{0} Image courtesy of NASA", DateTime.Today.Year);
      }

      public string Version = "4810";

      /// <summary>
      /// Bing Maps Customer Identification.
      /// |
      /// FOR LEGAL AND COMMERCIAL USAGE SET YOUR OWN REGISTERED KEY
      /// |
      /// http://msdn.microsoft.com/en-us/library/ff428642.aspx
      /// </summary>
      public string ClientKey = string.Empty;

      internal string SessionId = string.Empty;

      /// <summary>
      /// set true to append SessionId on requesting tiles
      /// </summary>
      public bool ForceSessionIdOnTileAccess = false;

      /// <summary>
      /// set true to avoid using dynamic tile url format
      /// </summary>
      public bool DisableDynamicTileUrlFormat = false;

      /// <summary>
      /// Converts tile XY coordinates into a QuadKey at a specified level of detail.
      /// </summary>
      /// <param name="tileX">Tile X coordinate.</param>
      /// <param name="tileY">Tile Y coordinate.</param>
      /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
      /// to 23 (highest detail).</param>
      /// <returns>A string containing the QuadKey.</returns>      
      internal string TileXYToQuadKey(long tileX, long tileY, int levelOfDetail)
      {
         StringBuilder quadKey = new StringBuilder();
         for(int i = levelOfDetail; i > 0; i--)
         {
            char digit = '0';
            int mask = 1 << (i - 1);
            if((tileX & mask) != 0)
            {
               digit++;
            }
            if((tileY & mask) != 0)
            {
               digit++;
               digit++;
            }
            quadKey.Append(digit);
         }
         return quadKey.ToString();
      }

      /// <summary>
      /// Converts a QuadKey into tile XY coordinates.
      /// </summary>
      /// <param name="quadKey">QuadKey of the tile.</param>
      /// <param name="tileX">Output parameter receiving the tile X coordinate.</param>
      /// <param name="tileY">Output parameter receiving the tile Y coordinate.</param>
      /// <param name="levelOfDetail">Output parameter receiving the level of detail.</param>
      internal void QuadKeyToTileXY(string quadKey, out int tileX, out int tileY, out int levelOfDetail)
      {
         tileX = tileY = 0;
         levelOfDetail = quadKey.Length;
         for(int i = levelOfDetail; i > 0; i--)
         {
            int mask = 1 << (i - 1);
            switch(quadKey[levelOfDetail - i])
            {
               case '0':
               break;

               case '1':
               tileX |= mask;
               break;

               case '2':
               tileY |= mask;
               break;

               case '3':
               tileX |= mask;
               tileY |= mask;
               break;

               default:
               throw new ArgumentException("Invalid QuadKey digit sequence.");
            }
         }
      }

      #region GMapProvider Members
      public override Guid Id
      {
         get
         {
            throw new NotImplementedException();
         }
      }

      public override string Name
      {
         get
         {
            throw new NotImplementedException();
         }
      }

      public override PureProjection Projection
      {
         get
         {
            return MercatorProjection.Instance;
         }
      }

      GMapProvider[] overlays;
      public override GMapProvider[] Overlays
      {
         get
         {
            if(overlays == null)
            {
               overlays = new GMapProvider[] { this };
            }
            return overlays;
         }
      }

      public override PureImage GetTileImage(GPoint pos, int zoom)
      {
         throw new NotImplementedException();
      }
      #endregion

      public bool TryCorrectVersion = true;

      /// <summary>
      /// set false to use your own key.
      /// FOR LEGAL AND COMMERCIAL USAGE SET YOUR OWN REGISTERED KEY
      /// http://msdn.microsoft.com/en-us/library/ff428642.aspx
      /// </summary>
      public bool TryGetDefaultKey = true;
      static bool init = false;

      public override void OnInitialized()
      {
         if(!init)
         {
            try
            {
               var key = ClientKey;

               // to avoid registration stuff, default key
               if(TryGetDefaultKey && string.IsNullOrEmpty(ClientKey))
               {
                  //old: Vx8dmDflxzT02jJUG8bEjMU07Xr9QWRpPTeRuAZTC1uZFQdDCvK/jUbHKdyHEWj4LvccTPoKofDHtzHsWu/0xuo5u2Y9rj88
                  key = Stuff.GString("Jq7FrGTyaYqcrvv9ugBKv4OVSKnmzpigqZtdvtcDdgZexmOZ2RugOexFSmVzTAhOWiHrdhFoNCoySnNF3MyyIOo5u2Y9rj88");
               }

               #region -- try get sesion key --
               if(!string.IsNullOrEmpty(key))
               {
                  string keyResponse = GMaps.Instance.UseUrlCache ? Cache.Instance.GetContent("BingLoggingServiceV1" + key, CacheType.UrlCache, TimeSpan.FromHours(8)) : string.Empty;

                  if(string.IsNullOrEmpty(keyResponse))
                  {
                     // Bing Maps WPF Control
                     // http://dev.virtualearth.net/webservices/v1/LoggingService/LoggingService.svc/Log?entry=0&auth={0}&fmt=1&type=3&group=MapControl&name=WPF&version=1.0.0.0&session=00000000-0000-0000-0000-000000000000&mkt=en-US

                     keyResponse = GetContentUsingHttp(string.Format("http://dev.virtualearth.net/webservices/v1/LoggingService/LoggingService.svc/Log?entry=0&fmt=1&type=3&group=MapControl&name=AJAX&mkt=en-us&auth={0}&jsonp=microsoftMapsNetworkCallback", key));

                     if(!string.IsNullOrEmpty(keyResponse) && keyResponse.Contains("ValidCredentials"))
                     {
                        if(GMaps.Instance.UseUrlCache)
                        {
                           Cache.Instance.SaveContent("BingLoggingServiceV1" + key, CacheType.UrlCache, keyResponse);
                        }
                     }
                  }

                  if(!string.IsNullOrEmpty(keyResponse) && keyResponse.Contains("sessionId") && keyResponse.Contains("ValidCredentials"))
                  {
                     // microsoftMapsNetworkCallback({"sessionId" : "xxx", "authenticationResultCode" : "ValidCredentials"})

                     SessionId = keyResponse.Split(',')[0].Split(':')[1].Replace("\"", string.Empty).Replace(" ", string.Empty);
                     Debug.WriteLine("GMapProviders.BingMap.SessionId: " + SessionId);
                  }
                  else
                  {
                     Debug.WriteLine("BingLoggingServiceV1: " + keyResponse);
                  }
               }
               #endregion

               // supporting old road
               if(TryCorrectVersion && DisableDynamicTileUrlFormat)
               {
                  #region -- get the version --
                  string url = @"http://www.bing.com/maps";
                  string html = GMaps.Instance.UseUrlCache ? Cache.Instance.GetContent(url, CacheType.UrlCache, TimeSpan.FromDays(7)) : string.Empty;

                  if(string.IsNullOrEmpty(html))
                  {
                     html = GetContentUsingHttp(url);
                     if(!string.IsNullOrEmpty(html))
                     {
                        if(GMaps.Instance.UseUrlCache)
                        {
                           Cache.Instance.SaveContent(url, CacheType.UrlCache, html);
                        }
                     }
                  }

                  if(!string.IsNullOrEmpty(html))
                  {
                     #region -- match versions --

                     Regex reg = new Regex("tilegeneration:(\\d*)", RegexOptions.IgnoreCase);
                     Match mat = reg.Match(html);
                     if(mat.Success)
                     {
                        GroupCollection gc = mat.Groups;
                        int count = gc.Count;
                        if(count == 2)
                        {
                           string ver = gc[1].Value;
                           string old = GMapProviders.BingMap.Version;
                           if(ver != old)
                           {
                              GMapProviders.BingMap.Version = ver;
                              GMapProviders.BingSatelliteMap.Version = ver;
                              GMapProviders.BingHybridMap.Version = ver;
#if DEBUG
                              Debug.WriteLine("GMapProviders.BingMap.Version: " + ver + ", old: " + old + ", consider updating source");
                              if(Debugger.IsAttached)
                              {
                                 Thread.Sleep(5555);
                              }
#endif
                           }
                           else
                           {
                              Debug.WriteLine("GMapProviders.BingMap.Version: " + ver + ", OK");
                           }
                        }
                     }
                     #endregion
                  }
                  #endregion
               }

               init = true; // try it only once
            }
            catch(Exception ex)
            {
               Debug.WriteLine("TryCorrectBingVersions failed: " + ex);
            }
         }
      }

      protected override bool CheckTileImageHttpResponse(WebResponse response)
      {
         var pass = base.CheckTileImageHttpResponse(response);
         if(pass)
         {
            var tileInfo = response.Headers.Get("X-VE-Tile-Info");
            if(tileInfo != null)
            {
               return !tileInfo.Equals("no-tile");
            }
         }
         return pass;
      }

      internal string GetTileUrl(string imageryType)
      {
         //Retrieve map tile URL from the Imagery Metadata service: http://msdn.microsoft.com/en-us/library/ff701716.aspx
         //This ensures that the current tile URL is always used.
         //This will prevent the app from breaking when the map tiles change.

         string ret = string.Empty;
         if(!string.IsNullOrEmpty(SessionId))
         {
            try
            {
               string url = "http://dev.virtualearth.net/REST/V1/Imagery/Metadata/" + imageryType + "?output=xml&key=" + SessionId;

               string r = GMaps.Instance.UseUrlCache ? Cache.Instance.GetContent("GetTileUrl" + imageryType, CacheType.UrlCache, TimeSpan.FromDays(7)) : string.Empty;
               bool cache = false;

               if(string.IsNullOrEmpty(r))
               {
                  r = GetContentUsingHttp(url);
                  cache = true;
               }

               if(!string.IsNullOrEmpty(r))
               {
                  XmlDocument doc = new XmlDocument();
                  doc.LoadXml(r);

                  XmlNode xn = doc["Response"];
                  string statuscode = xn["StatusCode"].InnerText;
                  if(string.Compare(statuscode, "200", true) == 0)
                  {
                     xn = xn["ResourceSets"]["ResourceSet"]["Resources"];
                     XmlNodeList xnl = xn.ChildNodes;
                     foreach(XmlNode xno in xnl)
                     {
                        XmlNode imageUrl = xno["ImageUrl"];

                        if(imageUrl != null && !string.IsNullOrEmpty(imageUrl.InnerText))
                        {
                           if(cache && GMaps.Instance.UseUrlCache)
                           {
                              Cache.Instance.SaveContent("GetTileUrl" + imageryType, CacheType.UrlCache, r);
                           }

                           var baseTileUrl = imageUrl.InnerText;

                           if(baseTileUrl.Contains("{key}") || baseTileUrl.Contains("{token}"))
                           {
                              baseTileUrl.Replace("{key}", SessionId).Replace("{token}", SessionId);
                           }
                           else if(ForceSessionIdOnTileAccess)
                           {
                              // haven't seen anyone doing that, yet? ;/                            
                              baseTileUrl += "&key=" + SessionId;
                           }

                           Debug.WriteLine("GetTileUrl, UrlFormat[" + imageryType + "]: " + baseTileUrl);

                           ret = baseTileUrl;
                           break;
                        }
                     }
                  }
               }
            }
            catch(Exception ex)
            {
               Debug.WriteLine("GetTileUrl: Error getting Bing Maps tile URL - " + ex);
            }
         }
         return ret;
      }

      #region RoutingProvider

      public MapRoute GetRoute(PointLatLng start, PointLatLng end, bool avoidHighways, bool walkingMode, int Zoom)
      {
         string tooltip;
         int numLevels;
         int zoomFactor;
         MapRoute ret = null;
         List<PointLatLng> points = GetRoutePoints(MakeRouteUrl(start, end, LanguageStr, avoidHighways, walkingMode), Zoom, out tooltip, out numLevels, out zoomFactor);
         if(points != null)
         {
            ret = new MapRoute(points, tooltip);
         }
         return ret;
      }

      public MapRoute GetRoute(string start, string end, bool avoidHighways, bool walkingMode, int Zoom)
      {
         string tooltip;
         int numLevels;
         int zoomFactor;
         MapRoute ret = null;
         List<PointLatLng> points = GetRoutePoints(MakeRouteUrl(start, end, LanguageStr, avoidHighways, walkingMode), Zoom, out tooltip, out numLevels, out zoomFactor);
         if(points != null)
         {
            ret = new MapRoute(points, tooltip);
         }
         return ret;
      }

      string MakeRouteUrl(string start, string end, string language, bool avoidHighways, bool walkingMode)
      {
         string addition = avoidHighways ? "&avoid=highways" : string.Empty;
         string mode = walkingMode ? "Walking" : "Driving";

         return string.Format(CultureInfo.InvariantCulture, RouteUrlFormatPointQueries, mode, start, end, addition, SessionId);
      }

      string MakeRouteUrl(PointLatLng start, PointLatLng end, string language, bool avoidHighways, bool walkingMode)
      {
         string addition = avoidHighways ? "&avoid=highways" : string.Empty;
         string mode = walkingMode ? "Walking" : "Driving";

         return string.Format(CultureInfo.InvariantCulture, RouteUrlFormatPointLatLng, mode, start.Lat, start.Lng, end.Lat, end.Lng, addition, SessionId);
      }

      List<PointLatLng> GetRoutePoints(string url, int zoom, out string tooltipHtml, out int numLevel, out int zoomFactor)
      {
         List<PointLatLng> points = null;
         tooltipHtml = string.Empty;
         numLevel = -1;
         zoomFactor = -1;
         try
         {
            string route = GMaps.Instance.UseRouteCache ? Cache.Instance.GetContent(url, CacheType.RouteCache) : string.Empty;

            if(string.IsNullOrEmpty(route))
            {
               route = GetContentUsingHttp(url);

               if(!string.IsNullOrEmpty(route))
               {
                  if(GMaps.Instance.UseRouteCache)
                  {
                     Cache.Instance.SaveContent(url, CacheType.RouteCache, route);
                  }
               }
            }

            // parse values
            if(!string.IsNullOrEmpty(route))
            {
               #region -- title --
               int tooltipEnd = 0;
               {
                  int x = route.IndexOf("<RoutePath><Line>") + 17;
                  if(x >= 17)
                  {
                     tooltipEnd = route.IndexOf("</Line></RoutePath>", x + 1);
                     if(tooltipEnd > 0)
                     {
                        int l = tooltipEnd - x;
                        if(l > 0)
                        {
                           //tooltipHtml = route.Substring(x, l).Replace(@"\x26#160;", " ");
                           tooltipHtml = route.Substring(x, l);
                        }
                     }
                  }
               }
               #endregion

               #region -- points --
               XmlDocument doc = new XmlDocument();
               doc.LoadXml(route);
               XmlNode xn = doc["Response"];
               string statuscode = xn["StatusCode"].InnerText;
               switch(statuscode)
               {
                  case "200":
                  {
                     xn = xn["ResourceSets"]["ResourceSet"]["Resources"]["Route"]["RoutePath"]["Line"];
                     XmlNodeList xnl = xn.ChildNodes;
                     if(xnl.Count > 0)
                     {
                        points = new List<PointLatLng>();
                        foreach(XmlNode xno in xnl)
                        {
                           XmlNode latitude = xno["Latitude"];
                           XmlNode longitude = xno["Longitude"];
                           points.Add(new PointLatLng(double.Parse(latitude.InnerText, CultureInfo.InvariantCulture),
                                                      double.Parse(longitude.InnerText, CultureInfo.InvariantCulture)));
                        }
                     }
                     break;
                  }
                  // no status implementation on routes yet although when introduced these are the codes. Exception will be catched.
                  case "400":
                  throw new Exception("Bad Request, The request contained an error.");
                  case "401":
                  throw new Exception("Unauthorized, Access was denied. You may have entered your credentials incorrectly, or you might not have access to the requested resource or operation.");
                  case "403":
                  throw new Exception("Forbidden, The request is for something forbidden. Authorization will not help.");
                  case "404":
                  throw new Exception("Not Found, The requested resource was not found.");
                  case "500":
                  throw new Exception("Internal Server Error, Your request could not be completed because there was a problem with the service.");
                  case "501":
                  throw new Exception("Service Unavailable, There's a problem with the service right now. Please try again later.");
                  default:
                  points = null;
                  break; // unknown, for possible future error codes
               }
               #endregion
            }
         }
         catch(Exception ex)
         {
            points = null;
            Debug.WriteLine("GetRoutePoints: " + ex);
         }
         return points;
      }

      // example : http://dev.virtualearth.net/REST/V1/Routes/Driving?o=xml&wp.0=44.979035,-93.26493&wp.1=44.943828508257866,-93.09332862496376&optmz=distance&rpo=Points&key=[PROVIDEYOUROWNKEY!!]
      static readonly string RouteUrlFormatPointLatLng = "http://dev.virtualearth.net/REST/V1/Routes/{0}?o=xml&wp.0={1},{2}&wp.1={3},{4}{5}&optmz=distance&rpo=Points&key={6}";
      static readonly string RouteUrlFormatPointQueries = "http://dev.virtualearth.net/REST/V1/Routes/{0}?o=xml&wp.0={1}&wp.1={2}{3}&optmz=distance&rpo=Points&key={4}";

      #endregion RoutingProvider

      #region GeocodingProvider

      public GeoCoderStatusCode GetPoints(string keywords, out List<PointLatLng> pointList)
      {
         //Escape keywords to better handle special characters.
         return GetLatLngFromGeocoderUrl(MakeGeocoderUrl("q=" + Uri.EscapeDataString(keywords)), out pointList);
      }

      public PointLatLng? GetPoint(string keywords, out GeoCoderStatusCode status)
      {
         List<PointLatLng> pointList;
         status = GetPoints(keywords, out pointList);
         return pointList != null && pointList.Count > 0 ? pointList[0] : (PointLatLng?)null;
      }

      public GeoCoderStatusCode GetPoints(Placemark placemark, out List<PointLatLng> pointList)
      {
         return GetLatLngFromGeocoderUrl(MakeGeocoderDetailedUrl(placemark), out pointList);
      }

      public PointLatLng? GetPoint(Placemark placemark, out GeoCoderStatusCode status)
      {
         List<PointLatLng> pointList;
         status = GetLatLngFromGeocoderUrl(MakeGeocoderDetailedUrl(placemark), out pointList);
         return pointList != null && pointList.Count > 0 ? pointList[0] : (PointLatLng?)null;
      }

      string MakeGeocoderDetailedUrl(Placemark placemark)
      {
         string parameters = string.Empty;

         if(!AddFieldIfNotEmpty(ref parameters, "countryRegion", placemark.CountryNameCode))
            AddFieldIfNotEmpty(ref parameters, "countryRegion", placemark.CountryName);

         AddFieldIfNotEmpty(ref parameters, "adminDistrict", placemark.DistrictName);
         AddFieldIfNotEmpty(ref parameters, "locality", placemark.LocalityName);
         AddFieldIfNotEmpty(ref parameters, "postalCode", placemark.PostalCodeNumber);

         if(!string.IsNullOrEmpty(placemark.HouseNo))
            AddFieldIfNotEmpty(ref parameters, "addressLine", placemark.ThoroughfareName + " " + placemark.HouseNo);
         else
            AddFieldIfNotEmpty(ref parameters, "addressLine", placemark.ThoroughfareName);

         return MakeGeocoderUrl(parameters);
      }

      bool AddFieldIfNotEmpty(ref string Input, string FieldName, string Value)
      {
         if(!string.IsNullOrEmpty(Value))
         {
            if(string.IsNullOrEmpty(Input))
               Input = string.Empty;
            else
               Input = Input + "&";

            Input = Input + FieldName + "=" + Value;

            return true;
         }
         return false;
      }

      public GeoCoderStatusCode GetPlacemarks(PointLatLng location, out List<Placemark> placemarkList)
      {
         // http://msdn.microsoft.com/en-us/library/ff701713.aspx
         throw new NotImplementedException();
      }

      public Placemark? GetPlacemark(PointLatLng location, out GeoCoderStatusCode status)
      {
         // http://msdn.microsoft.com/en-us/library/ff701713.aspx
         throw new NotImplementedException();
      }

      string MakeGeocoderUrl(string keywords)
      {
         return string.Format(CultureInfo.InvariantCulture, GeocoderUrlFormat, keywords, SessionId);
      }

      GeoCoderStatusCode GetLatLngFromGeocoderUrl(string url, out List<PointLatLng> pointList)
      {
         var status = GeoCoderStatusCode.Unknow;
         pointList = null;

         try
         {
            string geo = GMaps.Instance.UseGeocoderCache ? Cache.Instance.GetContent(url, CacheType.GeocoderCache) : string.Empty;

            bool cache = false;

            if(string.IsNullOrEmpty(geo))
            {
               geo = GetContentUsingHttp(url);

               if(!string.IsNullOrEmpty(geo))
               {
                  cache = true;
               }
            }

            status = GeoCoderStatusCode.Unknow;
            if(!string.IsNullOrEmpty(geo))
            {
               if(geo.StartsWith("<?xml") && geo.Contains("<Response"))
               {
                  XmlDocument doc = new XmlDocument();
                  doc.LoadXml(geo);
                  XmlNode xn = doc["Response"];
                  string statuscode = xn["StatusCode"].InnerText;
                  switch(statuscode)
                  {
                     case "200":
                     {
                        pointList = new List<PointLatLng>();
                        xn = xn["ResourceSets"]["ResourceSet"]["Resources"];
                        XmlNodeList xnl = xn.ChildNodes;
                        foreach(XmlNode xno in xnl)
                        {
                           XmlNode latitude = xno["Point"]["Latitude"];
                           XmlNode longitude = xno["Point"]["Longitude"];
                           pointList.Add(new PointLatLng(Double.Parse(latitude.InnerText, CultureInfo.InvariantCulture),
                                                         Double.Parse(longitude.InnerText, CultureInfo.InvariantCulture)));
                        }

                        if(pointList.Count > 0)
                        {
                           status = GeoCoderStatusCode.G_GEO_SUCCESS;
                           if(cache && GMaps.Instance.UseGeocoderCache)
                           {
                              Cache.Instance.SaveContent(url, CacheType.GeocoderCache, geo);
                           }
                           break;
                        }

                        status = GeoCoderStatusCode.G_GEO_UNKNOWN_ADDRESS;
                        break;
                     }

                     case "400":
                     status = GeoCoderStatusCode.G_GEO_BAD_REQUEST;
                     break; // bad request, The request contained an error.
                     case "401":
                     status = GeoCoderStatusCode.G_GEO_BAD_KEY;
                     break; // Unauthorized, Access was denied. You may have entered your credentials incorrectly, or you might not have access to the requested resource or operation.
                     case "403":
                     status = GeoCoderStatusCode.G_GEO_BAD_REQUEST;
                     break; // Forbidden, The request is for something forbidden. Authorization will not help.
                     case "404":
                     status = GeoCoderStatusCode.G_GEO_UNKNOWN_ADDRESS;
                     break; // Not Found, The requested resource was not found.
                     case "500":
                     status = GeoCoderStatusCode.G_GEO_SERVER_ERROR;
                     break; // Internal Server Error, Your request could not be completed because there was a problem with the service.
                     case "501":
                     status = GeoCoderStatusCode.Unknow;
                     break; // Service Unavailable, There's a problem with the service right now. Please try again later.
                     default:
                     status = GeoCoderStatusCode.Unknow;
                     break; // unknown, for possible future error codes
                  }
               }
            }
         }
         catch(Exception ex)
         {
            status = GeoCoderStatusCode.ExceptionInCode;
            Debug.WriteLine("GetLatLngFromGeocoderUrl: " + ex);
         }

         return status;
      }

      // http://dev.virtualearth.net/REST/v1/Locations/1%20Microsoft%20Way%20Redmond%20WA%2098052?o=xml&key=BingMapsKey
      static readonly string GeocoderUrlFormat = "http://dev.virtualearth.net/REST/v1/Locations?{0}&o=xml&key={1}";

      #endregion GeocodingProvider
   }

   /// <summary>
   /// BingMapProvider provider
   /// </summary>
   public class BingMapProvider : BingMapProviderBase
   {
      public static readonly BingMapProvider Instance;

      BingMapProvider()
      {
      }

      static BingMapProvider()
      {
         Instance = new BingMapProvider();
      }

      #region GMapProvider Members

      readonly Guid id = new Guid("D0CEB371-F10A-4E12-A2C1-DF617D6674A8");
      public override Guid Id
      {
         get
         {
            return id;
         }
      }

      readonly string name = "BingMap";
      public override string Name
      {
         get
         {
            return name;
         }
      }

      public override PureImage GetTileImage(GPoint pos, int zoom)
      {
         string url = MakeTileImageUrl(pos, zoom, LanguageStr);

         return GetTileImageUsingHttp(url);
      }

      public override void OnInitialized()
      {
         base.OnInitialized();

         if(!DisableDynamicTileUrlFormat)
         {
            //UrlFormat[Road]: http://ecn.{subdomain}.tiles.virtualearth.net/tiles/r{quadkey}.jpeg?g=3179&mkt={culture}&shading=hill

            UrlDynamicFormat = GetTileUrl("Road");
            if(!string.IsNullOrEmpty(UrlDynamicFormat))
            {
               UrlDynamicFormat = UrlDynamicFormat.Replace("{subdomain}", "t{0}").Replace("{quadkey}", "{1}").Replace("{culture}", "{2}");
            }
         }
      }

      #endregion

      string MakeTileImageUrl(GPoint pos, int zoom, string language)
      {
         string key = TileXYToQuadKey(pos.X, pos.Y, zoom);

         if(!DisableDynamicTileUrlFormat && !string.IsNullOrEmpty(UrlDynamicFormat))
         {
            return string.Format(UrlDynamicFormat, GetServerNum(pos, 4), key, language);
         }

         return string.Format(UrlFormat, GetServerNum(pos, 4), key, Version, language, ForceSessionIdOnTileAccess ? "&key=" + SessionId : string.Empty);
      }

      string UrlDynamicFormat = string.Empty;

      // http://ecn.t0.tiles.virtualearth.net/tiles/r120030?g=875&mkt=en-us&lbl=l1&stl=h&shading=hill&n=z

      static readonly string UrlFormat = "http://ecn.t{0}.tiles.virtualearth.net/tiles/r{1}?g={2}&mkt={3}&lbl=l1&stl=h&shading=hill&n=z{4}";
   }
}