// Copyright (C) Stichting Deltares 2016. All rights reserved. // // This file is part of Ringtoets. // // Ringtoets 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 3 of the License, or // (at your option) any later version. // // This program 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 this program. If not, see . // // All names, logos, and references to "Deltares" are registered trademarks of // Stichting Deltares and remain full property of Stichting Deltares at all times. // All rights reserved. using System; using System.Collections.Concurrent; using System.IO; using System.Threading; using Amib.Threading; using BruTile; using BruTile.Cache; using Core.Components.BruTile.Data; using Core.Components.BruTile.IO.Properties; namespace Core.Components.BruTile.IO { /// /// Class responsible for fetching map tiles asynchronously from a . /// /// /// Original source: https://github.com/FObermaier/DotSpatial.Plugins/blob/master/DotSpatial.Plugins.BruTileLayer/TileFetcher.cs /// Original license: http://www.apache.org/licenses/LICENSE-2.0.html /// public class AsyncTileFetcher : ITileFetcher { private readonly ConcurrentDictionary activeTileRequests = new ConcurrentDictionary(); private readonly ConcurrentDictionary openTileRequests = new ConcurrentDictionary(); private ITileProvider provider; private MemoryCache volatileCache; private ITileCache persistentCache; private SmartThreadPool threadPool; public event EventHandler TileReceived; public event EventHandler QueueEmpty; /// /// Creates an instance of . /// /// The tile provider. /// Minimum number of tiles in memory cache. /// Maximum number of tiles in memory cache. /// Optional: the persistent cache. When null, no tiles /// will be cached outside of the volatile memory cache. /// Throw when /// is null. /// Thrown when either /// or is negative. /// Thrown when /// is not smaller than . public AsyncTileFetcher(ITileProvider provider, int minTiles, int maxTiles, ITileCache permaCache = null) { if (provider == null) { throw new ArgumentNullException(nameof(provider)); } if (minTiles < 0) { throw new ArgumentOutOfRangeException(nameof(minTiles), Resources.AsyncTileFetcher_Number_of_tiles_for_memory_cache_cannot_be_negative); } if (maxTiles < 0) { throw new ArgumentOutOfRangeException(nameof(maxTiles), Resources.AsyncTileFetcher_Number_of_tiles_for_memory_cache_cannot_be_negative); } if (minTiles >= maxTiles) { throw new ArgumentException(Resources.AsyncTileFetcher_Minimum_number_of_tiles_in_memory_cache_must_be_less_than_maximum); } this.provider = provider; volatileCache = new MemoryCache(minTiles, maxTiles); persistentCache = permaCache ?? NoopTileCache.Instance; threadPool = new SmartThreadPool(10000, BruTileSettings.MaximumNumberOfThreads); } public byte[] GetTile(TileInfo tileInfo) { ThrowExceptionIfDisposed(); try { byte[] res = GetTileFromCache(tileInfo); if (res != null) { return res; } } catch (IOException) { return null; } ScheduleTileRequest(tileInfo); return null; } public bool IsReady() { ThrowExceptionIfDisposed(); return activeTileRequests.Count == 0 && openTileRequests.Count == 0; } public void DropAllPendingTileRequests() { ThrowExceptionIfDisposed(); // Notes: http://dotspatial.codeplex.com/discussions/473428 threadPool.Cancel(false); int dummy; foreach (var request in activeTileRequests.ToArray()) { if (!openTileRequests.ContainsKey(request.Key)) { if (!activeTileRequests.TryRemove(request.Key, out dummy)) { activeTileRequests.TryRemove(request.Key, out dummy); } } } openTileRequests.Clear(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (IsDisposed) { return; } if (disposing) { volatileCache.Clear(); threadPool.Dispose(); threadPool = null; volatileCache = null; provider = null; persistentCache = null; } IsDisposed = true; } private bool IsDisposed { get; set; } /// /// Throws an when /// is true. /// /// Thrown when calling this method while /// this instance is disposed. private void ThrowExceptionIfDisposed() { if (IsDisposed) { throw new ObjectDisposedException(GetType().Name); } } /// /// Gets the tile from the cache. /// /// The to get the tile for. /// An of which represent the tile. /// Thrown when an I/O error occurred while opening the file. /// Thrown when this operation is not /// supported on the current platform or the caller does not have the required permission. private byte[] GetTileFromCache(TileInfo tileInfo) { TileIndex index = tileInfo.Index; return volatileCache.Find(index) ?? persistentCache.Find(index); } private void ScheduleTileRequest(TileInfo tileInfo) { if (!HasTileAlreadyBeenRequested(tileInfo.Index)) { activeTileRequests.TryAdd(tileInfo.Index, 1); var threadArguments = new object[] { tileInfo }; threadPool.QueueWorkItem(GetTileOnThread, threadArguments); } } private bool HasTileAlreadyBeenRequested(TileIndex tileIndex) { return activeTileRequests.ContainsKey(tileIndex) || openTileRequests.ContainsKey(tileIndex); } /// /// Method to actually get the tile from the . /// /// The thread parameters. The first argument is the /// for the file to be fetched. private void GetTileOnThread(object[] parameters) { var tileInfo = (TileInfo) parameters[0]; GetTileOnThreadCore(tileInfo); } private void GetTileOnThreadCore(TileInfo tileInfo) { if (!Thread.CurrentThread.IsAlive) { return; } byte[] result = TryRequestTileData(tileInfo); MarkTileRequestHandled(tileInfo); if (result != null) { volatileCache.Add(tileInfo.Index, result); persistentCache.Add(tileInfo.Index, result); OnTileReceived(new TileReceivedEventArgs(tileInfo, result)); } } private byte[] TryRequestTileData(TileInfo tileInfo) { byte[] result = null; try { openTileRequests.TryAdd(tileInfo.Index, 1); result = provider.GetTile(tileInfo); } catch {} //Try at least once again if (result == null) { try { result = provider.GetTile(tileInfo); } catch {} } return result; } private void MarkTileRequestHandled(TileInfo tileInfo) { int dummy; if (!activeTileRequests.TryRemove(tileInfo.Index, out dummy)) { //try again activeTileRequests.TryRemove(tileInfo.Index, out dummy); } if (!openTileRequests.TryRemove(tileInfo.Index, out dummy)) { //try again openTileRequests.TryRemove(tileInfo.Index, out dummy); } } private void OnTileReceived(TileReceivedEventArgs tileReceivedEventArgs) { TileReceived?.Invoke(this, tileReceivedEventArgs); if (IsReady()) { OnQueueEmpty(EventArgs.Empty); } } private void OnQueueEmpty(EventArgs eventArgs) { QueueEmpty?.Invoke(this, eventArgs); } } }