Creating Bing Maps Tile-Layers in Windows Phone 7

A few months ago I read a wonderful article by Johannes Kebeck “Creating Bing Maps Tile-Layers from SQL Server 2008 on the Fly” and DataConnector project  in the codeplex. DataConnector project is focused on Bing Maps and SQL Server 2008 (and can also be easily used with SQL Azure). This allows organizations and developers to rapidly create web mapping applications that contain rich geospatial data visualization features. There is a sample with the Bing Maps Silverlight control, used to create a thematic maps, based on a spatial data in the SQL Server 2008. In the sample are demonstrated different ways to create a thematic map over a Bing Maps using MapLayers (vector data) and tile-layers (MapTileLayers).

When I started to deal with applications for Windows Phone 7 popped the question how to visualize geographic data. Microsoft offers Bing Maps control for Windows Phone 7.
I decided to try to create an application for Windows Phone, to create thematic maps on Bing Maps control as a tile-layers, using WCF service to create tiles of spatial data in SQL Server 2008 R2

 Requirements:

Software:

Sample data:

Sample data is a database SampleGeographyData from DataConnector project, modified with a data for country populations. 

Steps to reproduce:

  • Create a new Silverlight Windows Phone Application
  • Add a Bing Map component (Bing Map control) on the default page with a MapTileLayer inside it.
  • Restore a sample database, named SampleGeographyData
  • Add a WCF Service Application, named TileWcfService
  • Implement a logic to create a tiles with a color, based on a specific value.
  • Add UI controls to maintain thematic maps.
  • Create a class library, named Configuration and implement a logic to support external configuration in an app.config file.
  • Run the application

 

Create a new Silverlight Windows Phone Application

 

Add a Bing Map component (Bing Map control) on the default page with a MapTileLayer inside it.

 

Restore a sample database, named SampleGeographyData

 

Table countries contains fields NAME (the name of the country), POPULATION(the population of the country), the_geo(geometry type).

Fields NAME and POPULATION will be used to create criteria for thematic maps.

 

Add a WCF Service Application, named TileWcfService

 

 

Implement a logic to create a tiles with a color, based on a specific value.

ITile interface definition:

   1: [ServiceContract]
   2: public interface ITile
   3: {
   4:  
   5:     /// <summary>
   6:     /// ITile - Tile Interface
   7:     /// </summary>
   8:     /// <param name="table">string table - DB table to query</param>
   9:     /// <param name="quadkey">string quadkey</param>
  10:     /// <param name="thematicstr">string thematicstr - true || area || population</param>
  11:     /// <returns>Stream - tile png</returns>
  12:     [OperationContract]
  13:     [WebGet(UriTemplate = "{table}/{quadkey}/{thematicstr}")]
  14:  
  15:     Stream GetTile(string table, string quadkey, string thematicstr);
  16: }

 

Tile class definition:

   1: public class Tile : ITile
   2: {
   3:     #region Member Variables
   4:  
   5:     /* global parameters */
   6:     private string geomField = "the_geo"; // default name of Geography DataType column in tables
   7:     private string srid = "4326"; // default name of SRID Constraint, EPSG:4326, of tables
   8:     private int totalPoints = 0;
   9:     private bool thematic = false;
  10:     private int lvl;
  11:     private int tileX;
  12:     private int tileY;
  13:     private int nwX;
  14:     private int nwY;
  15:     private double nwLon;
  16:     private double nwLat;
  17:     private double seLon;
  18:     private double seLat;
  19:     private int pixelX;
  20:     private int pixelY;
  21:     private Dictionary<string, LoadStyle> LayerStyle = new Dictionary<string, LoadStyle>();
  22:  
  23:     //8 color values in thematic range
  24:     private string[] colorRange = new string[] { "#FF400000", "#FF804040", "#FFC04000", "#FFC08080", "#FFFF0000", "#FFFF8040", "#FFFFC080", "#FFFFFF80" };
  25:  
  26:  
  27:     #endregion //Member Variables
  28:  
  29:     ////Dummy default method
  30:     //public void DoWork()
  31:     //{
  32:     //}
  33:  
  34:     /// <summary>
  35:     /// Get png tile 
  36:     /// </summary>
  37:     /// <param name="table">layer table to use</param>
  38:     /// <param name="quadkey">quadkey for current tile</param>
  39:     /// <param name="thematicstr">string true or false - thematic styling</param>
  40:     /// <returns>png stream</returns>
  41:     /// <remarks>
  42:     ///   webHttp uses RESTful urel format
  43:     ///   thematic map selection does not use caching
  44:     /// </remarks>
  45:     public Stream GetTile(string table, string quadkey, string thematicstr)
  46:     {
  47:         DateTime queryStart = DateTime.Now;
  48:         DateTime queryStop;
  49:  
  50:         //1281396894
  51:         //LoadStyle - stroke, fill, opacity, pointRadius, valueCol, maxValue, skew factor
  52:         if (thematicstr.Equals("population"))
  53:         {
  54:             LayerStyle.Add("countries", new LoadStyle("#FFFF0000", "#FF00FF00", 0.25, 1, "POPULATION", 1281396.894, 128.0));
  55:         }
  56:         else if (thematicstr.Equals("area"))
  57:         {
  58:             LayerStyle.Add("countries", new LoadStyle("#FFFF0000", "#FF00FF00", 0.25, 1, "AREA", 6043.6, 128.0));
  59:            // LayerStyle.Add("countries", new LoadStyle("#FFFF0000", "#FF00FF00", 0.25, 1, "POPULATION", 1281396.894, 128.0));
  60:         }
  61:  
  62:         LayerStyle.Add("statesprovinces", new LoadStyle("#FFFF0000", "#FF0000FF", 0.25, 1, "AREA", 321.6, 128.0));
  63:         LayerStyle.Add("uscounties", new LoadStyle("#FFFF0000", "#FF00FFFF", 0.25, 2, "AREA", 74.22, 512.0));
  64:         LayerStyle.Add("faults", new LoadStyle("#FFFF0000", "#FF000000", 1.0, 2, "ACODE", 6, 8.0));
  65:         LayerStyle.Add("earthquakes", new LoadStyle("#FFFF0000", "#FFFFFF00", 1.0, 12, "OTHER_MAG1", 9.24, 10.0));
  66:  
  67:         thematic = !thematicstr.ToLower().Equals("false");
  68:         Encoding encoding = new UTF8Encoding();
  69:         Bitmap tileBitmap = new Bitmap(256, 256, PixelFormat.Format32bppArgb);
  70:         Graphics g = Graphics.FromImage(tileBitmap);
  71:         string connStr = ConfigurationManager.ConnectionStrings["DataConnectionString"].ConnectionString;
  72:         SqlConnection conn = new SqlConnection(connStr);
  73:         SqlDataReader rdr = null;
  74:         try
  75:         {
  76:             conn.Open();
  77:             MemoryStream imageAsMemoryStream = new MemoryStream();
  78:             if (!thematic && isCached(table, quadkey, conn))
  79:             {
  80:                 // get cached tile from table blob
  81:                 imageAsMemoryStream = GetCachedTile(table, quadkey, conn);
  82:             }
  83:             else
  84:             {
  85:                 //build tile since it is not in tile table or this is thematic map
  86:                 StringBuilder query = new StringBuilder("SELECT ID," + LayerStyle[table].valueCol + "," + geomField + ".Reduce(@reduce) as " + geomField + " FROM [dbo].[" + table + "] WITH(INDEX(the_geo_sidx)) WHERE ");
  87:                 query.Append(geomField + ".STIntersects(geography::STGeomFromText('POLYGON(('+@nwLon+' '+@nwLat+', '+@nwLon+' '+@seLat+', '+@seLon+' '+@seLat+', '+@seLon+' '+@nwLat+', '+@nwLon+' '+@nwLat+'))', @srid))=1");
  88:                 
  89:                 lvl = quadkey.Length;
  90:                 double reduce = int.Parse(ConfigurationSettings.AppSettings["reduceMax"]) - (lvl * 2000);
  91:                 if (reduce < 0) reduce = 0;
  92:  
  93:                 QuadKeyToTileXY(quadkey, out tileX, out tileY, out lvl);
  94:                 TileXYToPixelXY(tileX, tileY, out nwX, out nwY);
  95:  
  96:                 PixelXYToLatLong(nwX, nwY, lvl, out nwLat, out nwLon);
  97:                 PixelXYToLatLong(nwX + 256, nwY + 256, lvl, out seLat, out seLon);
  98:                 if (nwLon == -180.0) nwLon = -179.9;
  99:                 queryStart = DateTime.Now;
 100:                 SqlCommand cmd = new SqlCommand(query.ToString(), conn);
 101:                 cmd.Parameters.Add(new SqlParameter("reduce", reduce));
 102:                 cmd.Parameters.Add(new SqlParameter("srid", srid));
 103:                 cmd.Parameters.Add(new SqlParameter("nwLon", nwLon.ToString()));
 104:                 cmd.Parameters.Add(new SqlParameter("nwLat", nwLat.ToString()));
 105:                 cmd.Parameters.Add(new SqlParameter("seLon", seLon.ToString()));
 106:                 cmd.Parameters.Add(new SqlParameter("seLat", seLat.ToString()));
 107:                 //log.Info(query.ToString());
 108:                 //log.Info(quadkey + " reduce=" + reduce + " srid=" + srid + " nwLon=" + nwLon + " nwLat=" + nwLat + " seLon=" + seLon + " seLat=" + seLat);
 109:  
 110:                 rdr = cmd.ExecuteReader();
 111:  
 112:                 while (rdr.Read())
 113:                 {
 114:                     double value = 1.0;
 115:                     Color color = Color.White;
 116:                     if (thematic) // thematic color by colValue range
 117:                     {
 118:                         try
 119:                         {
 120:                             value = double.Parse(rdr[LayerStyle[table].valueCol].ToString()) / LayerStyle[table].maxValue;
 121:                         }
 122:                         catch (Exception e)
 123:                         {
 124:                             value = 1.0;
 125:                         }
 126:                         for (int i = 1; i < colorRange.Length; i++)
 127:                         {
 128:                             if (value < (i / (LayerStyle[table].skewFactor)))
 129:                             {
 130:                                 color = ColorFromInt(colorRange[i - 1]);
 131:                                 break;
 132:                             }
 133:                             else color = ColorFromInt(colorRange[colorRange.Length - 1]);
 134:                         }
 135:                     }
 136:  
 137:                     SqlGeography geo = (SqlGeography)rdr[geomField];
 138:                     //log.Debug(geo.STGeometryType().ToString().ToUpper());
 139:                     switch (geo.STGeometryType().ToString().ToUpper())
 140:                     {
 141:                         case "POINT":
 142:                             {
 143:                                 RenderPoint(geo, table, g, color);
 144:                                 break;
 145:                             }
 146:                         case "LINESTRING":
 147:                             {
 148:                                 RenderLinestring(geo, table, g, (int)rdr["ID"], color);
 149:                                 break;
 150:                             }
 151:                         case "POLYGON":
 152:                             {
 153:                                 RenderPolygon(geo, table, g, (int)rdr["ID"], color);
 154:                                 break;
 155:                             }
 156:                         case "MULTILINESTRING":
 157:                             {
 158:                                 RenderMultiLinestring(geo, table, g, (int)rdr["ID"], color);
 159:                                 break;
 160:                             }
 161:                         case "MULTIPOLYGON":
 162:                             {
 163:                                 RenderMultiPolygon(geo, table, g, (int)rdr["ID"], color);
 164:                                 break;
 165:                             }
 166:                         case "GEOMETRYCOLLECTION":
 167:                             {
 168:                                 RenderGeometryCollection(geo, table, g, (int)rdr["ID"], color);
 169:                                 break;
 170:                             }
 171:                     }
 172:  
 173:                 }
 174:                 queryStop = DateTime.Now;
 175:                 //log.Debug(String.Format("Query Time: {0,0:0}ms", (queryStop - queryStart).TotalMilliseconds));
 176:  
 177:                 tileBitmap.Save(imageAsMemoryStream, ImageFormat.Png);
 178:                 if (rdr != null) rdr.Close();
 179:  
 180:                 if (!thematic && !isCached(table, quadkey, conn))
 181:                 {
 182:                     // cache tile to table blob
 183:                     int iresult = SetCachedTile(table, quadkey, imageAsMemoryStream, conn);
 184:                 }
 185:             }
 186:  
 187:             imageAsMemoryStream.Position = 0;
 188:             tileBitmap.Dispose();
 189:             return imageAsMemoryStream;
 190:         }
 191:         catch (Exception e)
 192:         {
 193:             //log.Error(e.Message);
 194:         }
 195:         finally
 196:         {
 197:             if (rdr != null) rdr.Close();
 198:             if (conn != null) conn.Close();
 199:         }
 200:         Assembly thisExe = Assembly.GetExecutingAssembly();
 201:         return thisExe.GetManifestResourceStream("TileWcfService.empty.png");
 202:     }
 203:  
 204:     /// <summary>
 205:     /// isCached
 206:     ///     determines if a cached tile exists in the tileTable
 207:     /// </summary>
 208:     /// <param name="table">string layer table</param>
 209:     /// <param name="quadkey">string current quadkey</param>
 210:     /// <param name="conn"> SQL Server Connection</param>
 211:     /// <returns>bool true if tile exists in table</returns>
 212:     private bool isCached(string table, string quadkey, SqlConnection conn)
 213:     {
 214:         bool cached = false;
 215:         SqlCommand cmd = new SqlCommand("Select count(*) FROM tile" + table + " WHERE quadkey=@quadkey", conn);
 216:         cmd.Parameters.Add(new SqlParameter("quadkey", quadkey));
 217:  
 218:         int cnt = (int)cmd.ExecuteScalar();
 219:         if (cnt > 0) cached = true;
 220:         return cached;
 221:     }
 222:  
 223:     /// <summary>
 224:     /// GetCachedTile
 225:     ///     returns a tile from caching tileTable
 226:     /// </summary>
 227:     /// <param name="table">string layer table</param>
 228:     /// <param name="quadkey">string current quadkey</param>
 229:     /// <param name="conn"> SQL Server Connection</param>
 230:     /// <returns>png stream</returns>
 231:     private MemoryStream GetCachedTile(string table, string quadkey, SqlConnection conn)
 232:     {
 233:         SqlCommand cmdSelect = new SqlCommand("SELECT tile FROM tile" + table + " WHERE quadkey=@quadkey", conn);
 234:         cmdSelect.Parameters.Add(new SqlParameter("quadkey", quadkey));
 235:  
 236:         byte[] image = (byte[])cmdSelect.ExecuteScalar();
 237:         MemoryStream imageStream = new MemoryStream();
 238:         imageStream.Write(image, 0, image.Length);
 239:         return imageStream;
 240:     }
 241:  
 242:     /// <summary>
 243:     /// SetCachedTile
 244:     ///     Adds tile png to caching tileTable
 245:     /// </summary>
 246:     /// <param name="table">string layer table</param>
 247:     /// <param name="quadkey">string current quadkey</param>
 248:     /// <param name="imageStream">MemorySTream of png tile</param>
 249:     /// <param name="conn"> SQL Server Connection</param>
 250:     /// <returns>int ExecuteNonQuery() return</returns>
 251:     private int SetCachedTile(string table, string quadkey, MemoryStream imageStream, SqlConnection conn)
 252:     {
 253:  
 254:         imageStream.Position = 0;
 255:         byte[] imageData = new byte[imageStream.Length];
 256:         imageStream.Read(imageData, 0, (int)imageStream.Length);
 257:  
 258:         SqlCommand cmd = new SqlCommand("INSERT INTO tile" + table + "(quadkey,tile) values(@quadkey,@tile)", conn);
 259:         cmd.Parameters.Add(new SqlParameter("quadkey", quadkey));
 260:         cmd.Parameters.Add("@tile", SqlDbType.Image);
 261:         cmd.Parameters["@tile"].Value = imageData;
 262:         return cmd.ExecuteNonQuery();
 263:     }
 264:  
 265:  
 266:     #region render geography
 267:     /// <summary>
 268:     /// RenderPoint
 269:     ///     Render a point to Graphics
 270:     /// </summary>
 271:     /// <param name="geo">SqlGeography geo</param>
 272:     /// <param name="table">string layer table</param>
 273:     /// <param name="g">Graphics used to draw to</param>
 274:     /// <param name="valueFill">Color the fill color style</param>
 275:     private void RenderPoint(SqlGeography geo, string table, Graphics g, Color valueFill)
 276:     {
 277:         totalPoints++;
 278:         double lat = (double)geo.Lat;
 279:         double lon = (double)geo.Long;
 280:         LatLongToPixelXY(lat, lon, lvl, out pixelX, out pixelY);
 281:         Point cp = new Point(pixelX - nwX, pixelY - nwY);
 282:         if (valueFill.Equals(Color.White)) valueFill = ColorFromInt(LayerStyle[table].fill);
 283:         SolidBrush myBrush = new SolidBrush(valueFill);
 284:         int r = LayerStyle[table].pointRadius;
 285:         g.FillEllipse(myBrush, new Rectangle(cp.X - r / 2, cp.Y - r / 2, r, r));
 286:     }
 287:  
 288:     /// <summary>
 289:     /// RenderLinestring
 290:     ///     Render a linestring to Graphics
 291:     /// </summary>
 292:     /// <param name="geo">SqlGeography geo</param>
 293:     /// <param name="table">string layer table</param>
 294:     /// <param name="g">Graphics used to draw to</param>
 295:     /// <param name="valueFill">Color the stroke color style</param>
 296:     private void RenderLinestring(SqlGeography geo, string table, Graphics g, int id, Color valueFill)
 297:     {
 298:         if (geo.STNumPoints() > 1)
 299:         {
 300:             totalPoints += (int)geo.STNumPoints();
 301:             Point[] ptArray = new Point[(int)geo.STNumPoints()];
 302:             double lon1 = 0.0;
 303:             for (int j = 1; j <= geo.STNumPoints(); j++)
 304:             {
 305:                 double lat = (double)geo.STPointN(j).Lat;
 306:                 double lon = (double)geo.STPointN(j).Long;
 307:                 if (j > 1)
 308:                 {
 309:                     lon = HemisphereCorrection(lon, lon1, id);
 310:                 }
 311:                 LatLongToPixelXY(lat, lon, lvl, out pixelX, out pixelY);
 312:                 ptArray[j - 1] = new Point(pixelX - nwX, pixelY - nwY);
 313:                 lon1 = lon;
 314:             }
 315:             if (valueFill.Equals(Color.White)) valueFill = ColorFromInt(LayerStyle[table].fill);
 316:             GraphicsPath linePath = new GraphicsPath();
 317:             linePath.AddLines(ptArray);
 318:             Pen myPen = new Pen(valueFill);
 319:             myPen.Width = 2;
 320:             g.DrawPath(myPen, linePath);
 321:         }
 322:  
 323:     }
 324:  
 325:     /// <summary>
 326:     /// RenderPolygon
 327:     ///     Render a polygon to Graphics
 328:     /// </summary>
 329:     /// <param name="geo">SqlGeography geo</param>
 330:     /// <param name="table">string layer table</param>
 331:     /// <param name="g">Graphics used to draw to</param>
 332:     /// <param name="valueFill">Color the fill color style</param>
 333:     private void RenderPolygon(SqlGeography geo, string table, Graphics g, int id, Color valueFill)
 334:     {
 335:  
 336:         if (geo.NumRings() > 0)
 337:         {
 338:             totalPoints += (int)geo.STNumPoints();
 339:             for (int j = 1; j <= geo.NumRings(); j++)
 340:             {
 341:                 if (geo.RingN(j).STNumPoints() > 1)
 342:                 {
 343:                     Point[] ptArray = new Point[(int)geo.RingN(j).STNumPoints()];
 344:                     double lon1 = 0.0;
 345:                     for (int k = 1; k <= geo.RingN(j).STNumPoints(); k++)
 346:                     {
 347:                         double lat = (double)geo.RingN(j).STPointN(k).Lat;
 348:                         double lon = (double)geo.RingN(j).STPointN(k).Long;
 349:                         if (k > 1)
 350:                         {
 351:                             lon = HemisphereCorrection(lon, lon1, id);
 352:                         }
 353:                         LatLongToPixelXY(lat, lon, lvl, out pixelX, out pixelY);
 354:                         ptArray[k - 1] = new Point(pixelX - nwX, pixelY - nwY);
 355:                         lon1 = lon;
 356:                     }
 357:                     if (valueFill.Equals(Color.White)) valueFill = ColorFromInt(LayerStyle[table].fill);
 358:                     GraphicsPath polygonRegion = new GraphicsPath();
 359:                     polygonRegion.AddPolygon(ptArray);
 360:                     Region region = new Region(polygonRegion);
 361:  
 362:                     SolidBrush myBrush = new SolidBrush(valueFill);
 363:                     g.FillRegion(myBrush, region);
 364:                     Pen myPen = new Pen(ColorFromInt(LayerStyle[table].stroke));
 365:                     myPen.Width = 1;
 366:                     g.DrawPolygon(myPen, ptArray);
 367:                 }
 368:             }
 369:  
 370:         }
 371:  
 372:     }
 373:  
 374:     /// <summary>
 375:     /// RenderMultiLinestring
 376:     ///     Render a MultiLinestring to Graphics
 377:     /// </summary>
 378:     /// <param name="geo">SqlGeography geo</param>
 379:     /// <param name="table">string layer table</param>
 380:     /// <param name="g">Graphics used to draw to</param>
 381:     /// <param name="id">int record id</param>
 382:     /// <param name="valueFill">Color the stroke color style</param>
 383:     private void RenderMultiLinestring(SqlGeography geo, string table, Graphics g, int id, Color valueFill)
 384:     {
 385:  
 386:         if (geo.STNumGeometries() > 0)
 387:         {
 388:             totalPoints += (int)geo.STNumPoints();
 389:  
 390:             for (int j = 1; j <= geo.STNumGeometries(); j++)
 391:             {
 392:                 if (geo.STGeometryN(j).NumRings() > 0)
 393:                 {
 394:                     for (int k = 1; k <= geo.STGeometryN(j).NumRings(); k++)
 395:                     {
 396:                         if (geo.STGeometryN(j).RingN(k).STNumPoints() > 1)
 397:                         {
 398:                             Point[] ptArray = new Point[(int)geo.STNumPoints()];
 399:                             double lon1 = 0.0;
 400:                             for (int m = 1; m <= geo.STGeometryN(j).RingN(k).STNumPoints(); m++)
 401:                             {
 402:                                 double lat = (double)geo.STGeometryN(j).RingN(k).STPointN(m).Lat;
 403:                                 double lon = (double)geo.STGeometryN(j).RingN(k).STPointN(m).Long;
 404:                                 if (m > 1)
 405:                                 {
 406:                                     lon = HemisphereCorrection(lon, lon1, id);
 407:                                 }
 408:                                 LatLongToPixelXY(lat, lon, lvl, out pixelX, out pixelY);
 409:                                 ptArray[m - 1] = new Point(pixelX - nwX, pixelY - nwY);
 410:                                 lon1 = lon;
 411:                             }
 412:                             if (valueFill.Equals(Color.White)) valueFill = ColorFromInt(LayerStyle[table].fill);
 413:                             GraphicsPath linePath = new GraphicsPath();
 414:                             linePath.AddLines(ptArray);
 415:                             Pen myPen = new Pen(valueFill);
 416:                             myPen.Width = 2;
 417:                             g.DrawPath(myPen, linePath);
 418:                         }
 419:                     }
 420:                 }
 421:             }
 422:  
 423:         }
 424:  
 425:     }
 426:  
 427:     /// <summary>
 428:     /// RenderMultiPolygon
 429:     ///     Render a Multipolygon to Graphics
 430:     /// </summary>
 431:     /// <param name="geo">SqlGeography geo</param>
 432:     /// <param name="table">string layer table</param>
 433:     /// <param name="g">Graphics used to draw to</param>
 434:     /// <param name="id">int record id</param>
 435:     /// <param name="valueFill">Color the fill color style</param>
 436:     private void RenderMultiPolygon(SqlGeography geo, string table, Graphics g, int id, Color valueFill)
 437:     {
 438:  
 439:         if (geo.STNumGeometries() > 0)
 440:         {
 441:             totalPoints += (int)geo.STNumPoints();
 442:  
 443:             for (int j = 1; j <= geo.STNumGeometries(); j++)
 444:             {
 445:                 if (geo.STGeometryN(j).NumRings() > 0)
 446:                 {
 447:                     for (int k = 1; k <= geo.STGeometryN(j).NumRings(); k++)
 448:                     {
 449:                         if (geo.STGeometryN(j).RingN(k).STNumPoints() > 1)
 450:                         {
 451:                             Point[] ptArray = new Point[(int)geo.STGeometryN(j).RingN(k).STNumPoints()];
 452:                             double lon1 = 0.0;
 453:                             int count = (int)geo.STGeometryN(j).RingN(k).STNumPoints();
 454:                             for (int m = 1; m <= geo.STGeometryN(j).RingN(k).STNumPoints(); m++)
 455:                             {
 456:                                 double lat = (double)geo.STGeometryN(j).RingN(k).STPointN(m).Lat;
 457:                                 double lon = (double)geo.STGeometryN(j).RingN(k).STPointN(m).Long;
 458:                                 if (m > 1)
 459:                                 {
 460:                                     lon = HemisphereCorrection(lon, lon1, id);
 461:                                 }
 462:                                 LatLongToPixelXY(lat, lon, lvl, out pixelX, out pixelY);
 463:                                 ptArray[m - 1] = new Point(pixelX - nwX, pixelY - nwY);
 464:                                 lon1 = lon;
 465:                             }
 466:                             if (valueFill.Equals(Color.White)) valueFill = ColorFromInt(LayerStyle[table].fill);
 467:                             GraphicsPath polygonRegion = new GraphicsPath();
 468:                             polygonRegion.AddPolygon(ptArray);
 469:                             Region region = new Region(polygonRegion);
 470:  
 471:                             g.FillRegion(new SolidBrush(valueFill), region);
 472:                             Pen myPen = new Pen(ColorFromInt(LayerStyle[table].stroke));
 473:                             myPen.Width = 1;
 474:                             g.DrawPolygon(myPen, ptArray);
 475:                         }
 476:                     }
 477:                 }
 478:             }
 479:         }
 480:  
 481:     }
 482:  
 483:  
 484:     /// <summary>
 485:     /// RenderGeometryCollection
 486:     ///     Render a GeometryCollection to Graphics
 487:     /// </summary>
 488:     /// <param name="geo">SqlGeography geo</param>
 489:     /// <param name="table">string layer table</param>
 490:     /// <param name="g">Graphics used to draw to</param>
 491:     /// <param name="id">int record id</param>
 492:     /// <param name="valueFill">Color the fill color style</param>
 493:     private void RenderGeometryCollection(SqlGeography geo, string table, Graphics g, int id, Color valueFill)
 494:     {
 495:         int numGeom = (int)geo.STNumGeometries();
 496:         if (geo.STNumGeometries() > 0)
 497:         {
 498:             for (int j = 1; j <= geo.STNumGeometries(); j++)
 499:             {
 500:                 if (geo.STGeometryN(j).NumRings() > 0)
 501:                 {
 502:                     for (int k = 1; k <= geo.STGeometryN(j).NumRings(); k++)
 503:                     {
 504:                         if (geo.STGeometryN(j).RingN(k).STNumPoints() > 1)
 505:                         {
 506:                             double lon1 = 0.0;
 507:                             Point[] ptArray = new Point[(int)geo.STGeometryN(j).RingN(k).STNumPoints()];
 508:                             for (int m = 1; m <= geo.STGeometryN(j).RingN(k).STNumPoints(); m++)
 509:                             {
 510:                                 double lat = (double)geo.STGeometryN(j).RingN(k).STPointN(m).Lat;
 511:                                 double lon = (double)geo.STGeometryN(j).RingN(k).STPointN(m).Long;
 512:  
 513:                                 if (m > 1)
 514:                                 {
 515:                                     lon = HemisphereCorrection(lon, lon1, id);
 516:                                 }
 517:  
 518:                                 LatLongToPixelXY(lat, lon, lvl, out pixelX, out pixelY);
 519:                                 ptArray[m - 1] = new Point(pixelX - nwX, pixelY - nwY);
 520:                                 lon1 = lon;
 521:                             }
 522:                             if (valueFill.Equals(Color.White)) valueFill = ColorFromInt(LayerStyle[table].fill);
 523:                             GraphicsPath extRingRegion = new GraphicsPath();
 524:                             extRingRegion.AddPolygon(ptArray);
 525:                             Region region = new Region(extRingRegion);
 526:                             g.FillRegion(new SolidBrush(valueFill), region);
 527:                             Pen myPen = new Pen(ColorFromInt(LayerStyle[table].stroke));
 528:                             myPen.Width = 1;
 529:                             g.DrawPolygon(myPen, ptArray);
 530:                         }
 531:                     }
 532:                 }
 533:             }
 534:         }
 535:     }
 536:  
 537:     /// <summary>
 538:     /// HemisphereCorrection
 539:     ///     attempts to correct polygons crossing International Dataline
 540:     /// </summary>
 541:     /// <param name="lon"></param>
 542:     /// <param name="lon1"></param>
 543:     /// <param name="id"></param>
 544:     /// <returns></returns>
 545:     private double HemisphereCorrection(double lon, double lon1, int id)
 546:     {
 547:         //truncate polygon to nearest hemisphere boundary
 548:         if ((lon < 0.0 && lon1 > 0.0) || (lon > 0.0 && lon1 < 0.0))
 549:         { // crosses hemisphere - use shorter of distances to opposite hemisphere boundaries
 550:             //log.Debug("Crosses Hemisphere: " + lon + " " + lon1 + " id=" + id);
 551:             double d1 = Math.Abs(lon1 - 180);
 552:             double d2 = Math.Abs(lon1 - 0.0);
 553:             if (lon1 > 0)
 554:             {
 555:                 if (d1 < d2) lon = 180.0;
 556:                 else lon = 0.0;
 557:             }
 558:             else
 559:             {
 560:                 if (d1 < d2) lon = -180.0;
 561:                 else lon = 0.0;
 562:             }
 563:         }
 564:         return lon;
 565:     }
 566:  
 567:  
 568:     #endregion
 569:  
 570:     #region Helper Functions
 571:  
 572:  
 573:  
 574:     /// <summary>
 575:     /// Clips a number to the specified minimum and maximum values.
 576:     /// </summary>
 577:     /// <param name="n">The number to clip.</param>
 578:     /// <param name="minValue">Minimum allowable value.</param>
 579:     /// <param name="maxValue">Maximum allowable value.</param>
 580:     /// <returns>The clipped value.</returns>
 581:     /// <remarks>
 582:     ///     Most helper functions are from MSDN site:
 583:     ///     http://msdn.microsoft.com/en-us/library/bb259689.aspx
 584:     ///</remarks>
 585:     private static double Clip(double n, double minValue, double maxValue)
 586:     {
 587:         return Math.Min(Math.Max(n, minValue), maxValue);
 588:     }
 589:  
 590:  
 591:  
 592:     /// <summary>
 593:     /// Determines the map width and height (in pixels) at a specified level
 594:     /// of detail.
 595:     /// </summary>
 596:     /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
 597:     /// to 23 (highest detail).</param>
 598:     /// <returns>The map width and height in pixels.</returns>
 599:     public static uint MapSize(int levelOfDetail)
 600:     {
 601:         return (uint)256 << levelOfDetail;
 602:     }
 603:  
 604:  
 605:  
 606:     /// <summary>
 607:     /// Determines the ground resolution (in meters per pixel) at a specified
 608:     /// latitude and level of detail.
 609:     /// </summary>
 610:     /// <param name="latitude">Latitude (in degrees) at which to measure the
 611:     /// ground resolution.</param>
 612:     /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
 613:     /// to 23 (highest detail).</param>
 614:     /// <returns>The ground resolution, in meters per pixel.</returns>
 615:     public static double GroundResolution(double latitude, int levelOfDetail)
 616:     {
 617:         latitude = Clip(latitude, MinLatitude, MaxLatitude);
 618:         return Math.Cos(latitude * Math.PI / 180) * 2 * Math.PI * EarthRadius / MapSize(levelOfDetail);
 619:     }
 620:  
 621:  
 622:  
 623:     /// <summary>
 624:     /// Determines the map scale at a specified latitude, level of detail,
 625:     /// and screen resolution.
 626:     /// </summary>
 627:     /// <param name="latitude">Latitude (in degrees) at which to measure the
 628:     /// map scale.</param>
 629:     /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
 630:     /// to 23 (highest detail).</param>
 631:     /// <param name="screenDpi">Resolution of the screen, in dots per inch.</param>
 632:     /// <returns>The map scale, expressed as the denominator N of the ratio 1 : N.</returns>
 633:     public static double MapScale(double latitude, int levelOfDetail, int screenDpi)
 634:     {
 635:         return GroundResolution(latitude, levelOfDetail) * screenDpi / 0.0254;
 636:     }
 637:  
 638:  
 639:  
 640:     /// <summary>
 641:     /// Converts a point from latitude/longitude WGS-84 coordinates (in degrees)
 642:     /// into pixel XY coordinates at a specified level of detail.
 643:     /// </summary>
 644:     /// <param name="latitude">Latitude of the point, in degrees.</param>
 645:     /// <param name="longitude">Longitude of the point, in degrees.</param>
 646:     /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
 647:     /// to 23 (highest detail).</param>
 648:     /// <param name="pixelX">Output parameter receiving the X coordinate in pixels.</param>
 649:     /// <param name="pixelY">Output parameter receiving the Y coordinate in pixels.</param>
 650:     public static void LatLongToPixelXY(double latitude, double longitude, int levelOfDetail, out int pixelX, out int pixelY)
 651:     {
 652:         latitude = Clip(latitude, MinLatitude, MaxLatitude);
 653:         longitude = Clip(longitude, MinLongitude, MaxLongitude);
 654:  
 655:         double x = (longitude + 180) / 360;
 656:         double sinLatitude = Math.Sin(latitude * Math.PI / 180);
 657:         double y = 0.5 - Math.Log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI);
 658:  
 659:         uint mapSize = MapSize(levelOfDetail);
 660:         pixelX = (int)Clip(x * mapSize + 0.5, 0, mapSize - 1);
 661:         pixelY = (int)Clip(y * mapSize + 0.5, 0, mapSize - 1);
 662:     }
 663:  
 664:  
 665:  
 666:     /// <summary>
 667:     /// Converts a pixel from pixel XY coordinates at a specified level of detail
 668:     /// into latitude/longitude WGS-84 coordinates (in degrees).
 669:     /// </summary>
 670:     /// <param name="pixelX">X coordinate of the point, in pixels.</param>
 671:     /// <param name="pixelY">Y coordinates of the point, in pixels.</param>
 672:     /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
 673:     /// to 23 (highest detail).</param>
 674:     /// <param name="latitude">Output parameter receiving the latitude in degrees.</param>
 675:     /// <param name="longitude">Output parameter receiving the longitude in degrees.</param>
 676:     public static void PixelXYToLatLong(int pixelX, int pixelY, int levelOfDetail, out double latitude, out double longitude)
 677:     {
 678:         double mapSize = MapSize(levelOfDetail);
 679:         double x = (Clip(pixelX, 0, mapSize - 1) / mapSize) - 0.5;
 680:         double y = 0.5 - (Clip(pixelY, 0, mapSize - 1) / mapSize);
 681:  
 682:         latitude = 90 - 360 * Math.Atan(Math.Exp(-y * 2 * Math.PI)) / Math.PI;
 683:         longitude = 360 * x;
 684:     }
 685:  
 686:     private const double EarthRadius = 6378137;
 687:     private const double MinLatitude = -85.05112878;
 688:     private const double MaxLatitude = 85.05112878;
 689:     private const double MinLongitude = -180;
 690:     private const double MaxLongitude = 180;
 691:  
 692:     /// <summary>
 693:     /// Converts pixel XY coordinates into tile XY coordinates of the tile containing
 694:     /// the specified pixel.
 695:     /// </summary>
 696:     /// <param name="pixelX">Pixel X coordinate.</param>
 697:     /// <param name="pixelY">Pixel Y coordinate.</param>
 698:     /// <param name="tileX">Output parameter receiving the tile X coordinate.</param>
 699:     /// <param name="tileY">Output parameter receiving the tile Y coordinate.</param>
 700:     public static void PixelXYToTileXY(int pixelX, int pixelY, out int tileX, out int tileY)
 701:     {
 702:         tileX = pixelX / 256;
 703:         tileY = pixelY / 256;
 704:     }
 705:  
 706:  
 707:  
 708:     /// <summary>
 709:     /// Converts tile XY coordinates into pixel XY coordinates of the upper-left pixel
 710:     /// of the specified tile.
 711:     /// </summary>
 712:     /// <param name="tileX">Tile X coordinate.</param>
 713:     /// <param name="tileY">Tile Y coordinate.</param>
 714:     /// <param name="pixelX">Output parameter receiving the pixel X coordinate.</param>
 715:     /// <param name="pixelY">Output parameter receiving the pixel Y coordinate.</param>
 716:     public static void TileXYToPixelXY(int tileX, int tileY, out int pixelX, out int pixelY)
 717:     {
 718:         pixelX = tileX * 256;
 719:         pixelY = tileY * 256;
 720:     }
 721:  
 722:  
 723:  
 724:     /// <summary>
 725:     /// Converts tile XY coordinates into a QuadKey at a specified level of detail.
 726:     /// </summary>
 727:     /// <param name="tileX">Tile X coordinate.</param>
 728:     /// <param name="tileY">Tile Y coordinate.</param>
 729:     /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
 730:     /// to 23 (highest detail).</param>
 731:     /// <returns>A string containing the QuadKey.</returns>
 732:     public static string TileXYToQuadKey(int tileX, int tileY, int levelOfDetail)
 733:     {
 734:         StringBuilder quadKey = new StringBuilder();
 735:         for (int i = levelOfDetail; i > 0; i--)
 736:         {
 737:             char digit = '0';
 738:             int mask = 1 << (i - 1);
 739:             if ((tileX & mask) != 0)
 740:             {
 741:                 digit++;
 742:             }
 743:             if ((tileY & mask) != 0)
 744:             {
 745:                 digit++;
 746:                 digit++;
 747:             }
 748:             quadKey.Append(digit);
 749:         }
 750:         return quadKey.ToString();
 751:     }
 752:  
 753:  
 754:     /// <summary>
 755:     /// Converts a QuadKey into tile XY coordinates.
 756:     /// </summary>
 757:     /// <param name="quadKey">QuadKey of the tile.</param>
 758:     /// <param name="tileX">Output parameter receiving the tile X coordinate.</param>
 759:     /// <param name="tileY">Output parameter receiving the tile Y coordinate.</param>
 760:     /// <param name="levelOfDetail">Output parameter receiving the level of detail.</param>
 761:     public static void QuadKeyToTileXY(string quadKey, out int tileX, out int tileY, out int levelOfDetail)
 762:     {
 763:         tileX = tileY = 0;
 764:         levelOfDetail = quadKey.Length;
 765:         for (int i = levelOfDetail; i > 0; i--)
 766:         {
 767:             int mask = 1 << (i - 1);
 768:             switch (quadKey[levelOfDetail - i])
 769:             {
 770:                 case '0':
 771:                     break;
 772:  
 773:                 case '1':
 774:                     tileX |= mask;
 775:                     break;
 776:  
 777:                 case '2':
 778:                     tileY |= mask;
 779:                     break;
 780:  
 781:                 case '3':
 782:                     tileX |= mask;
 783:                     tileY |= mask;
 784:                     break;
 785:  
 786:                 default:
 787:                     throw new ArgumentException("Invalid QuadKey digit sequence.");
 788:             }
 789:         }
 790:     }
 791:  
 792:     /// <summary>
 793:     /// ColorFromInt
 794:     /// Returns a Color from hex string i.e. #FF00FF00
 795:     /// </summary>
 796:     /// <param name="hex">string hex color with alpha, red, green, blue</param>
 797:     /// <returns>Color</returns>
 798:     private Color ColorFromInt(string hex)
 799:     {
 800:         if (hex.StartsWith("#")) hex = hex.Substring(1);
 801:         int c = int.Parse(hex, NumberStyles.AllowHexSpecifier);
 802:         return Color.FromArgb((byte)((c >> 0x18) & 0xff),
 803:             (byte)((c >> 0x10) & 0xff),
 804:             (byte)((c >> 8) & 0xff),
 805:             (byte)(c & 0xff));
 806:     }
 807:  
 808:  
 809:     /// <summary>
 810:     /// Returns a random color
 811:     /// </summary>
 812:     /// <returns></returns>
 813:     public static Color RandomColor()
 814:     {
 815:         Random randomSeed = new Random();
 816:         return Color.FromArgb(
 817:             randomSeed.Next(256),
 818:             randomSeed.Next(256),
 819:             randomSeed.Next(256)
 820:         );
 821:     }
 822:  
 823:  
 824:     #endregion
 825:  
 826: }

 

Add UI controls to maintain thematic maps.

   1: <Grid x:Name="ContentPanel" Grid.Row="1" HorizontalAlignment="Stretch"
   2:        VerticalAlignment="Stretch" Margin="12,0,12,0">
   3:     <my:Map HorizontalAlignment="Stretch"  Name="MainMap" 
   4:             CredentialsProvider="AhyL1itKqs_HSBTekvefjurUR4O-eFGbahleUWXB5vB0e5zON9LSeWPwHghfQF_a"
   5:             VerticalAlignment="Stretch">
   6:         <my:Map.Children>
   7:  
   8:             <my:MapTileLayer x:Name="layerCountries" Visibility="Collapsed" Opacity="0.7" >
   9:                 <my:MapTileLayer.TileSources>
  10:                     <my:LocationRectTileSource  ZoomRange="1,19" />
  11:                 </my:MapTileLayer.TileSources>
  12:             </my:MapTileLayer>
  13:             <CheckBox x:Name="chkCountries"  Foreground="Black" VerticalAlignment="Bottom" 
  14:                 HorizontalAlignment="Left" Content="Countries" 
  15:                       Margin="5 5 5 70" IsChecked="False" 
  16:                       Click="ChkCountriesClick"/>
  17:             <CheckBox x:Name="chkThematic" Foreground="Black" VerticalAlignment="Bottom" 
  18:                 HorizontalAlignment="Left" Content="Thematic Tiles" 
  19:                       Margin="5 5 5 10" IsChecked="False" 
  20:                       Click="CheckBoxThematic_Click"/>
  21:  
  22:             <Button x:Name="btnDefaultZoom" Foreground="Black" VerticalAlignment="Bottom" 
  23:                 HorizontalAlignment="Right" Content="Default Zoom" 
  24:                       Margin="5 5 5 20" Click="BtnDefaultZoomClick"/>
  25:  
  26:             <Border  VerticalAlignment="Top" HorizontalAlignment="Right" Margin="0 13 10 0"  Opacity="0.8" BorderBrush="White" BorderThickness="2" CornerRadius="5">
  27:                 <StackPanel Margin="10 12 10 10">
  28:                     <!-- The TileLayer Radio button group. -->
  29:                    <RadioButton Foreground="Black" Name="Population_rBtn" GroupName = "TileLayers" Content="Color By Population" FontSize="12" Checked="CheckBoxThematic_Click" />
  30:                     <RadioButton Name="Area_rBtn" GroupName = "TileLayers" Content="Color By Area" Foreground="Black" FontSize="12" IsChecked="True" Checked="CheckBoxThematic_Click"/>
  31:                 </StackPanel>
  32:             </Border>
  33:         </my:Map.Children>
  34:     </my:Map>
  35: </Grid>

 

Implement a code in en event handler , called when you need to turn on / off generation of the tiles.

   1: #region ChkCountriesClick
   2: private void ChkCountriesClick(object sender, RoutedEventArgs e)
   3: {
   4:     var cb = sender as CheckBox;
   5:     const string layerName = "layerCountries";
   6:     if (cb != null)
   7:         if ((bool)cb.IsChecked)
   8:         {
   9:             //Layer Checked
  10:             //initiate a map ViewChange event
  11:             if (MainMap.FindName(layerName).GetType().Equals(typeof(MapTileLayer)))
  12:             {   //tiles 
  13:                 ((MapTileLayer)MainMap.FindName(layerName)).Visibility = Visibility.Visible;
  14:             }
  15:  
  16:             CheckBoxThematic_Click(this.chkThematic, null);
  17:  
  18:             MainMap.SetView(MainMap.Center, MainMap.ZoomLevel);
  19:  
  20:  
  21:         }
  22:         else
  23:         {
  24:             //Layer Unchecked
  25:             if (MainMap.FindName(layerName).GetType().Equals(typeof(MapTileLayer)))
  26:             {   //tiles 
  27:                 ((MapTileLayer)MainMap.FindName(layerName)).Visibility = Visibility.Collapsed;
  28:             }
  29:         }
  30: }
  31: #endregion //ChkCountriesClick

 

   1: #region CheckBoxThematic_Click
   2: private void CheckBoxThematic_Click(object sender, RoutedEventArgs e)
   3: {
   4:     var cb = sender as CheckBox;
   5:     var rb = sender as RadioButton;
   6:  
   7:     if (this.Area_rBtn == null || this.Population_rBtn == null)
   8:     {
   9:         return;
  10:     }
  11:  
  12:     if ((cb != null && (bool)cb.IsChecked) || ((rb != null && (bool)rb.IsChecked) && (bool)this.chkThematic.IsChecked))
  13:     {
  14:  
  15:         if (this.Area_rBtn.IsChecked == true)
  16:         {
  17:             SetMapTileLayers("area");
  18:         }
  19:         else if (this.Population_rBtn.IsChecked == true)
  20:         {
  21:             SetMapTileLayers("population");
  22:         }
  23:  
  24:  
  25:     }
  26:     else
  27:     {
  28:         SetMapTileLayers("false");
  29:     }
  30: }
  31: #endregion //CheckBoxThematic_Click

 

Implement method SetMapTileLayers that sets UriFormat to MapTileLayer to use WCF service that returns tiles:

   1: #region SetMapTileLayers
   2: private void SetMapTileLayers(string thematicstr)
   3: {
   4:     foreach (UIElement item in MainMap.Children)
   5:     {
   6:         if (item is MapTileLayer)
   7:         {
   8:             MapTileLayer mtile = item as MapTileLayer;
   9:             string layer = mtile.Name;
  10:             foreach (LocationRectTileSource tilesource in mtile.TileSources)
  11:             {
  12:                 tilesource.UriFormat = "http://localhost:23024/Services/Tile.svc/" + layer.Replace("layer", "").ToLower() + "/{quadkey}/" + thematicstr;
  13:             }
  14:         }
  15:     }
  16: }
  17: #endregion //SetMapTileLayers

Create a class library, named Configuration and implement a logic to support external configuration in an app.config file. 

When developing applications it is always a good idea not to hardcode database connections, URLs, server locations, etc... In .NET there's the Configuration stack that allows us to use the app.config xml file. This functionality is not supported in Silverlight as well as in the Windows Phone 7.

In  Alex Yakhnin's Blog there is an article with a sample how to implement Mobile Application Blocks that includes the Configuration block   (sample code is ported from Windows Mobile 6.x to Windows Phone 7).

Then you add app.config file to your project and make sure that it has the the Build Action set to "Content".

 

app.config file

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <configSections>
   4:     <section name="ApplicationSettings"  type="Configuration.ApplicationSettingsSection,ThematicMapsWindowPhone" />
   5:   </configSections>
   6:  
   7:   <ApplicationSettings>
   8:     <appSettings>
   9:       <add key="remoteServer" value="http://localhost:23024/Services/Tile.svc/"/>
  10:     </appSettings>
  11:   </ApplicationSettings>
  12:  
  13: </configuration>

The last step is to add the code that reads the values from this configuration file:  add an ApplicationSettingsSection to read values from app.config and modify SetMapTileLayers method to use a WCF host and  port from this file:

 

 

   1: namespace ThematicMapsWindowPhone.Views
   2: {
   3:     public partial class ThematicMapView : PhoneApplicationPage
   4:     {
   5:         #region Member Variables
   6:  
   7:         readonly ApplicationSettingsSection _section = (ApplicationSettingsSection)ConfigurationManager.GetSection("ApplicationSettings");
   8:  
   9:         #endregion //Member Variables
  10:  
  11: ...
  12:  
  13: #region SetMapTileLayers
  14: private void SetMapTileLayers(string thematicstr)
  15: {
  16:     foreach (UIElement item in MainMap.Children)
  17:     {
  18:         if (item is MapTileLayer)
  19:         {
  20:             MapTileLayer mtile = item as MapTileLayer;
  21:             string layer = mtile.Name;
  22:             foreach (LocationRectTileSource tilesource in mtile.TileSources)
  23:             {
  24:                 tilesource.UriFormat = _section.AppSettings["remoteServer"].Value + layer.Replace("layer", "").ToLower() + "/{quadkey}/" + thematicstr;
  25:             }
  26:         }
  27:     }
  28: }
  29: #endregion //SetMapTileLayers
  30:  
  31: }

  

Run the Application

Start the application

 

Check “Countries” CheckBox to receive tiles from SQL Server 2008 spatial data.

 

Check “Thematic Tiles” to create a thematic map (by default thematic map tiles color is by area.

 

Select Radio button “Color by Population” to create a thematic map by population.

 

Navigate changing the zoom (drag map for pan, double click for zoom in , click “default zoom “ button to go back to the default zoom level.

Enjoy! You have now a real thematic map on your Windows Phone 7 .

  

Source code of the demo application you could download here:ThematicMapsWindowPhone.zip

Sample database you download here:ThematicMapsWindowPhone.zip


Add a Comment

Be the first one to add a comment. Please Login or Register to add comment.