// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) 2014 OxyPlot contributors // // // Provides an annotation that shows a tile based map. // // -------------------------------------------------------------------------------------------------------------------- namespace ExampleLibrary { using OxyPlot; using OxyPlot.Annotations; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; using System.Threading; /// /// Provides an annotation that shows a tile based map. /// /// The longitude and latitude range of the map is defined by the range of the x and y axis, respectively. public class TileMapAnnotation : Annotation { /// /// The image cache. /// private readonly Dictionary images = new Dictionary(); /// /// The download queue. /// private readonly Queue queue = new Queue(); /// /// The current number of downloads /// private int numberOfDownloads; /// /// Initializes a new instance of the class. /// public TileMapAnnotation() { this.TileSize = 256; this.MinZoomLevel = 0; this.MaxZoomLevel = 20; this.Opacity = 1.0; this.MaxNumberOfDownloads = 8; this.UserAgent = "OxyPlotExampleLibrary"; } /// /// Gets or sets the max number of simultaneous downloads. /// /// The max number of downloads. public int MaxNumberOfDownloads { get; set; } /// /// Gets or sets the URL. /// /// The URL. public string Url { get; set; } /// /// Gets or sets the copyright notice. /// /// The copyright notice. public string CopyrightNotice { get; set; } /// /// Gets or sets the size of the tiles. /// /// The size of the tiles. public int TileSize { get; set; } /// /// Gets or sets the min zoom level. /// /// The min zoom level. public int MinZoomLevel { get; set; } /// /// Gets or sets the max zoom level. /// /// The max zoom level. public int MaxZoomLevel { get; set; } /// /// Gets or sets the opacity. /// /// The opacity. public double Opacity { get; set; } /// /// Gets or sets the user agent used for requests. /// public string UserAgent { get; set; } /// /// Renders the annotation on the specified context. /// /// The render context. public override void Render(IRenderContext rc) { var lon0 = this.XAxis.ActualMinimum; var lon1 = this.XAxis.ActualMaximum; var lat0 = this.YAxis.ActualMinimum; var lat1 = this.YAxis.ActualMaximum; // the desired number of tiles horizontally double tilesx = this.PlotModel.Width / this.TileSize; // calculate the desired zoom level var n = tilesx / (((lon1 + 180) / 360) - ((lon0 + 180) / 360)); var zoom = (int)Math.Round(Math.Log(n) / Math.Log(2)); if (zoom < this.MinZoomLevel) { zoom = this.MinZoomLevel; } if (zoom > this.MaxZoomLevel) { zoom = this.MaxZoomLevel; } // find tile coordinates for the corners double x0, y0; LatLonToTile(lat0, lon0, zoom, out x0, out y0); double x1, y1; LatLonToTile(lat1, lon1, zoom, out x1, out y1); double xmax = Math.Max(x0, x1); double xmin = Math.Min(x0, x1); double ymax = Math.Max(y0, y1); double ymin = Math.Min(y0, y1); var clippingRectangle = this.GetClippingRect(); // Add the tiles for (var x = (int)xmin; x < xmax; x++) { for (var y = (int)ymin; y < ymax; y++) { string uri = this.GetTileUri(x, y, zoom); var img = this.GetImage(uri, rc.RendersToScreen); if (img == null) { continue; } // transform from tile coordinates to lat/lon double latitude0, latitude1, longitude0, longitude1; TileToLatLon(x, y, zoom, out latitude0, out longitude0); TileToLatLon(x + 1, y + 1, zoom, out latitude1, out longitude1); // transform from lat/lon to screen coordinates var s00 = this.Transform(longitude0, latitude0); var s11 = this.Transform(longitude1, latitude1); var r = OxyRect.Create(s00.X, s00.Y, s11.X, s11.Y); // draw the image rc.DrawImage(img, r.Left, r.Top, r.Width, r.Height, this.Opacity, true); } } // draw the copyright notice var p = new ScreenPoint(clippingRectangle.Right - 5, clippingRectangle.Bottom - 5); var textSize = rc.MeasureText(this.CopyrightNotice, this.ActualFont, this.ActualFontSize, this.ActualFontWeight); rc.DrawRectangle( new OxyRect(p.X - textSize.Width - 2, p.Y - textSize.Height - 2, textSize.Width + 4, textSize.Height + 4), OxyColor.FromAColor(200, OxyColors.White), OxyColors.Undefined, 0, this.EdgeRenderingMode); rc.DrawText( p, this.CopyrightNotice, OxyColors.Black, this.ActualFont, this.ActualFontSize, this.ActualFontWeight, 0, HorizontalAlignment.Right, VerticalAlignment.Bottom); } /// /// Transforms a position to a tile coordinate. /// /// The latitude. /// The longitude. /// The zoom. /// The x. /// The y. private static void LatLonToTile(double latitude, double longitude, int zoom, out double x, out double y) { // http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames int n = 1 << zoom; double lat = latitude / 180 * Math.PI; x = (longitude + 180.0) / 360.0 * n; y = (1.0 - (Math.Log(Math.Tan(lat) + (1.0 / Math.Cos(lat))) / Math.PI)) / 2.0 * n; } /// /// Transforms a tile coordinate (x,y) to a position. /// /// The x. /// The y. /// The zoom. /// The latitude. /// The longitude. private static void TileToLatLon(double x, double y, int zoom, out double latitude, out double longitude) { int n = 1 << zoom; longitude = (x / n * 360.0) - 180.0; double lat = Math.Atan(Math.Sinh(Math.PI * (1 - (2 * y / n)))); latitude = lat * 180.0 / Math.PI; } /// /// Gets the image from the specified uri. /// /// The URI. /// Get the image asynchronously if set to true. The plot model will be invalidated when the image has been downloaded. /// The image. /// This method gets the image from cache, or starts an async download. private OxyImage GetImage(string uri, bool asyncLoading) { OxyImage img; if (this.images.TryGetValue(uri, out img)) { return img; } if (!asyncLoading) { return this.Download(uri); } lock (this.queue) { // 'reserve' an image (otherwise multiple downloads of the same uri may happen) this.images[uri] = null; this.queue.Enqueue(uri); } this.BeginDownload(); return null; } /// /// Downloads the image from the specified URI. /// /// The URI. /// The image private OxyImage Download(string uri) { OxyImage img = null; var mre = new ManualResetEvent(false); var request = (HttpWebRequest)WebRequest.Create(uri); request.Method = "GET"; request.BeginGetResponse( r => { try { if (request.HaveResponse) { var response = request.EndGetResponse(r); var stream = response.GetResponseStream(); var ms = new MemoryStream(); stream.CopyTo(ms); var buffer = ms.ToArray(); img = new OxyImage(buffer); this.images[uri] = img; } } catch (Exception e) { var ie = e; while (ie != null) { System.Diagnostics.Debug.WriteLine(ie.Message); ie = ie.InnerException; } } finally { mre.Set(); } }, request); mre.WaitOne(); return img; } /// /// Starts the next download in the queue. /// private void BeginDownload() { if (this.numberOfDownloads >= this.MaxNumberOfDownloads) { return; } string uri = this.queue.Dequeue(); var request = (HttpWebRequest)WebRequest.Create(uri); request.Method = "GET"; #if NETFRAMEWORK // unavailable in NET Standard 1.0 request.UserAgent = this.UserAgent; #else // compiles but does not run under NET Framework request.Headers["User-Agent"] = this.UserAgent; #endif Interlocked.Increment(ref this.numberOfDownloads); request.BeginGetResponse( r => { Interlocked.Decrement(ref this.numberOfDownloads); try { if (request.HaveResponse) { var response = request.EndGetResponse(r); var stream = response.GetResponseStream(); this.DownloadCompleted(uri, stream); } } catch (Exception e) { var ie = e; while (ie != null) { System.Diagnostics.Debug.WriteLine(ie.Message); ie = ie.InnerException; } } }, request); } /// /// The download completed, set the image. /// /// The URI. /// The result. private void DownloadCompleted(string uri, Stream result) { if (result == null) { return; } var ms = new MemoryStream(); result.CopyTo(ms); var buffer = ms.ToArray(); var img = new OxyImage(buffer); this.images[uri] = img; lock (this.queue) { // Clear old items in the queue, new ones will be added when the plot is refreshed foreach (var queuedUri in this.queue) { // Remove the 'reserved' image this.images.Remove(queuedUri); } this.queue.Clear(); } this.PlotModel.InvalidatePlot(false); if (this.queue.Count > 0) { this.BeginDownload(); } } /// /// Gets the tile URI. /// /// The tile x. /// The tile y. /// The zoom. /// The uri. private string GetTileUri(int x, int y, int zoom) { string url = this.Url.Replace("{X}", x.ToString(CultureInfo.InvariantCulture)); url = url.Replace("{Y}", y.ToString(CultureInfo.InvariantCulture)); return url.Replace("{Z}", zoom.ToString(CultureInfo.InvariantCulture)); } } }