// Copyright 2014 - Felix Obermaier (www.ivv-aachen.de) // // This file is part of SharpMap. // SharpMap is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // SharpMap is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with SharpMap; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.Serialization; using GeoAPI.Geometries; using SharpMap.Data; using SharpMap.Styles; namespace SharpMap.Layers { /// /// A proxy class to allow async tile rendering for static layers /// /// /// var map = new SharpMap.Map(new System.Drawing.Size(1024, 786)); /// var provider = new SharpMap.Data.Providers.Shapefile("<Path to shapefile>", true); /// var layer = new SharpMap.Layers.VectorLayer("LAYER1", provider); /// map.BackgroundLayer.Add(AsyncLayerProxyLayer.Create(layer)); /// [Serializable] public class AsyncLayerProxyLayer : ITileAsyncLayer, IDisposable, ICanQueryLayer { private readonly ILayer _baseLayer; private bool _onlyRedrawWhenComplete; [NonSerialized] private int _numPendingDownloads; private readonly Size _cellSize; private readonly Size _cellBuffer = new Size(5, 5); private ImageAttributes _imageAttributes = new ImageAttributes(); [NonSerialized] private object _renderLock = new object(); [NonSerialized] private Bitmap _bitmap; [NonSerialized] private Envelope _lastViewport = new Envelope(); private class RenderTask { /// /// The token to cancel the task /// internal System.Threading.CancellationTokenSource CancellationToken; /// /// The task /// internal System.Threading.Tasks.Task Task; } [NonSerialized] private List _currentTasks = new List(); /// /// Method to warp a usual layer in an async layer /// /// The layer to wrap /// The size of the tile /// A async tile layer public static ILayer Create(ILayer layer, Size? tileSize = null) { if (layer == null) throw new ArgumentNullException("layer"); if (layer is ITileAsyncLayer) return layer; tileSize = tileSize ?? new Size(256, 256); return new AsyncLayerProxyLayer(layer, tileSize.Value); } /// /// Creates an instance of this class /// /// The layer to proxy /// The size of the tile private AsyncLayerProxyLayer(ILayer baseLayer, Size cellSize) { _baseLayer = baseLayer; _cellSize = cellSize; ((ILayer)this).VisibilityUnits = baseLayer.VisibilityUnits; } double ILayer.MinVisible { get { return _baseLayer.MinVisible; } set { _baseLayer.MinVisible = value; } } double ILayer.MaxVisible { get { return _baseLayer.MaxVisible; } set { _baseLayer.MaxVisible = value; } } /// /// Gets or Sets what level-reference the Min/Max values are defined in /// VisibilityUnits ILayer.VisibilityUnits { get; set; } /// /// Specifies whether this layer should be rendered or not /// bool ILayer.Enabled { get { return _baseLayer.Enabled; } set { _baseLayer.Enabled = value; } } /// /// Optional title of the layer. It will be used for services like WMS where both Name and Title are supported. /// public string LayerTitle { get { return _baseLayer.LayerTitle; } set { _baseLayer.LayerTitle = value; } } /// /// Name of layer /// string ILayer.LayerName { get { return _baseLayer.LayerName; } set { _baseLayer.LayerName = value; } } /// /// Gets the boundingbox of the entire layer /// Envelope ILayer.Envelope { get { return _baseLayer.Envelope; } } /// /// The spatial reference ID (CRS) /// int ILayer.SRID { get { return _baseLayer.SRID; } set { _baseLayer.SRID = value; } } /// /// The spatial reference ID (CRS) that can be exposed externally. /// /// /// TODO: explain better why I need this property /// int ILayer.TargetSRID { get { return _baseLayer.TargetSRID; } } /// /// Proj4 String Projection /// string ILayer.Proj4Projection { get { return _baseLayer.Proj4Projection; } set { _baseLayer.Proj4Projection = value; } } /// /// Renders the layer /// /// Graphics object reference /// Map which is rendered void ILayer.Render(Graphics g, Map map) { ((ILayer)this).Render(g, (MapViewport)map); } void ILayer.Render(Graphics g, MapViewport map) { // We don't need to regenerate the tiles if (map.Envelope.Equals(_lastViewport) && _numPendingDownloads == 0) { g.DrawImage(_bitmap, Point.Empty); return; } // Create a backbuffer lock (_renderLock) { if (_bitmap == null || _bitmap.Size != map.Size) { _bitmap = new Bitmap(map.Size.Width, map.Size.Height, PixelFormat.Format32bppArgb); using (var tmpGraphics = Graphics.FromImage(_bitmap)) tmpGraphics.Clear(Color.Transparent); } } // Save the last viewport _lastViewport = map.Envelope; // Cancel old rendercycle ((ITileAsyncLayer)this).Cancel(); var mapViewport = map.Envelope; var mapSize = map.Size; var mapColumnWidth = _cellSize.Width+_cellBuffer.Width; var mapColumnHeight = _cellSize.Height + _cellBuffer.Width; var columns = (int)Math.Ceiling((double) mapSize.Width/mapColumnWidth); var rows = (int) Math.Ceiling((double) mapSize.Height/mapColumnHeight); var renderMapSize = new Size(columns * _cellSize.Width + _cellBuffer.Width, rows * _cellSize.Height + _cellBuffer.Height); var horizontalFactor = (double) renderMapSize.Width/mapSize.Width; var verticalFactor = (double) renderMapSize.Height/mapSize.Height; var diffX = 0.5d*(horizontalFactor*mapViewport.Width - mapViewport.Width); var diffY = 0.5d*(verticalFactor*mapViewport.Height-mapViewport.Height); var totalRenderMapViewport = mapViewport.Grow(diffX, diffY); var columnWidth = totalRenderMapViewport.Width/columns; var rowHeight = totalRenderMapViewport.Height/rows; var rmdx = (int)((mapSize.Width-renderMapSize.Width) * 0.5f); var rmdy = (int)((mapSize.Height - renderMapSize.Height) * 0.5f); var tileSize = Size.Add(_cellSize, Size.Add(_cellBuffer, _cellBuffer)); var miny = totalRenderMapViewport.MinY; var pty = rmdy + renderMapSize.Height - tileSize.Height; for (var i = 0; i < rows; i ++) { var minx = totalRenderMapViewport.MinX; var ptx = rmdx; for (var j = 0; j < columns; j++) { var tmpMap = new Map(_cellSize); tmpMap.Layers.Add(_baseLayer); tmpMap.DisposeLayersOnDispose = false; tmpMap.ZoomToBox(new Envelope(minx, minx + columnWidth, miny, miny + rowHeight)); var cancelToken = new System.Threading.CancellationTokenSource(); var token = cancelToken.Token; var pt = new Point(ptx, pty); var t = new System.Threading.Tasks.Task(delegate { if (token.IsCancellationRequested) token.ThrowIfCancellationRequested(); var res = RenderCellOnThread(token, pt, tmpMap); if (res) { System.Threading.Interlocked.Decrement(ref _numPendingDownloads); var e = DownloadProgressChanged; if (e != null) e(_numPendingDownloads); } }, token); var dt = new RenderTask {CancellationToken = cancelToken, Task = t}; lock (_currentTasks) { _currentTasks.Add(dt); _numPendingDownloads++; } t.Start(); minx += columnWidth; ptx += _cellSize.Width; } miny += rowHeight; pty -= _cellSize.Height; } } /// /// Event raised when a new tile has been rendered an is now avalable /// public event MapNewTileAvaliabledHandler MapNewTileAvaliable; /// /// Event raised when the rendering of tiles has made progress /// public event DownloadProgressHandler DownloadProgressChanged; bool ITileAsyncLayer.OnlyRedrawWhenComplete { get { return _onlyRedrawWhenComplete; } set { _onlyRedrawWhenComplete = value; } } void ITileAsyncLayer.Cancel() { lock (_currentTasks) { foreach (var t in _currentTasks) { if (!t.Task.IsCompleted) t.CancellationToken.Cancel(); } _currentTasks.Clear(); _numPendingDownloads = 0; } } int ITileAsyncLayer.NumPendingDownloads { get { return _numPendingDownloads; } } private bool RenderCellOnThread(System.Threading.CancellationToken token, Point ptInsert, Map map) { var tile = new Bitmap(map.Size.Width, map.Size.Height, PixelFormat.Format32bppArgb); var mvp = new MapViewport(map); using (var g = Graphics.FromImage(tile)) { g.Clear(Color.Transparent); _baseLayer.Render(g, mvp); map.Layers.Clear(); } if (!token.IsCancellationRequested) { OnTileRendered(ptInsert, map.Envelope, tile); return true; } return false; } /// /// Method to raise the map tile available event /// /// /// /// protected virtual void OnTileRendered(Point ptInsert, Envelope env, Bitmap bmp) { if (!env.Equals(_lastViewport)) { System.Threading.Monitor.Enter(_renderLock); using (var g = Graphics.FromImage(_bitmap)) g.DrawImageUnscaled(bmp, ptInsert); System.Threading.Monitor.Exit(_renderLock); } var h1 = MapNewTileAvaliable; if (h1 != null) { h1(null, env, bmp, bmp.Width, bmp.Height, _imageAttributes); } var h2 = DownloadProgressChanged; if (h2 != null) { System.Threading.Interlocked.Decrement(ref _numPendingDownloads); h2(_numPendingDownloads); } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// /// 2 public void Dispose() { ((ITileAsyncLayer)this).Cancel(); _imageAttributes.Dispose(); } void ICanQueryLayer.ExecuteIntersectionQuery(Envelope box, FeatureDataSet ds) { if (!((ICanQueryLayer)this).IsQueryEnabled) return; ((ICanQueryLayer)_baseLayer).ExecuteIntersectionQuery(box, ds); } void ICanQueryLayer.ExecuteIntersectionQuery(IGeometry geometry, FeatureDataSet ds) { if (!((ICanQueryLayer)this).IsQueryEnabled) return; ((ICanQueryLayer)_baseLayer).ExecuteIntersectionQuery(geometry, ds); } bool ICanQueryLayer.IsQueryEnabled { get { var cql = _baseLayer as ICanQueryLayer; if (cql != null && cql.IsQueryEnabled) return true; return false; } set { var cql = _baseLayer as ICanQueryLayer; if (cql != null ) cql.IsQueryEnabled = value; } } [OnDeserialized] private void OnDeserialized(StreamingContext context) { _imageAttributes = new ImageAttributes(); _currentTasks = new List(); _renderLock = new object(); _lastViewport = new Envelope(); } } }