Log in to like this post! Creating Bing Maps Tile-Layers in Windows Phone 7 [Infragistics] Mihail Mateev / Monday, December 27, 2010 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: Visual Studio 2010 Windows Phone Developer Tools Windows Phone 7 Toolkit Data Connector project in codeplex App.config for WP7 applications 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: Sample database you download here: