using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Threading; using TradeIdeas.ServerConnection; using TradeIdeas.TIProData.Interfaces; /* This collects all of the images that we download from the server. * * This based on AlertIcons.pas. That name refers to the icons representing each type * of alert. When I first created that unit that was the only type of image that we * needed. Since then we added other types of images, such as the icons for * filters, the patterns used for the background of the alerts windows, and the * small charts that we display. In any case these are all small GIF images. * * This unit attempts to put the requests into a reasonable order. The simplest * version of this is a stack. If the requests come in more quickly than we can * handle them, then we send them most recent ones to the server first. It is * possible that the older ones have already been scrolled off the screen. We * also allow the client to make a low priority request. This allows us to * preload the requests before an image is displayed on the screen. * * This is not as important as it once was. At one time we only sent one HTTP * request at a time regardless of anything. So if we requested a lot of * pictures that could get in the the way of the other data. Requests can * still get in the way, but not as much as before, because each thread on the * server has it's own queues, and the client can send multiple requests. * Like the Delphi version this limits us to 50 outstanding requests. That's * a somewhat arbitrary way of keeping these requests from going overboard. * * The Delphi version would group some of the images together into an image list. * This was required because windows will only create a limited number of bitmap * objects. Once we added the small charts, we could easily go over that * number. C# seems to work differently. It seems that C# will automatically * release the windows resources when it needs to. Then it will reread the * stream and recreate the image as needed. That's based on the documentation, * not experience. * * Most important, this acts as a cache. The first request from the client goes to the * server. The rest of the requests are handled from the cache. * * All icons requests are asynchronous. When you request an icon, you might * or might not get it. If the icon is in the cache, you will get it. If * necessary the request will be passed on to the server. In either case the * request returns immediately. * * When a new icon is available, we notify all regiestered listeners. For * simplicity we have only one callback, not one per icon. The listener must * be a control. We call invalidate for the control, so it will redraw itself. * Presumably that will request the icon again. * * Note: If you don't want to use the cache and the callbacks, then use * GenericNetRequest. This uses the same code on the server side. We send * the same message. But that code does not do any caching, and it has one * callback per request. * * The Delphi version had an extra function which would allow someone to * download all of the alert and filter icons to a directory all at once. * There are no plans to add that here. */ namespace TradeIdeas.TIProData { public delegate void CachedImageAvailable(); public class ImageCacheManager { public const string ALERTS = "/Alerts/"; public const string FILTERS = "/Filters/"; public const string TEXTURES = "/Texture/"; public const string GIF_CHARTS = "/GifCharts/"; public const int ICON_HEIGHT = 20; public const int ICON_WIDTH = 40; public const int DAILY_CHART_WIDTH = 66; public const int WEEKLY_CHART_WIDTH = 53; // Icons should be exactly this size. Daily and weekly charts come in different widths // and should typically be right justified on a black background. If you draw an icon // smaller than this, it should be scaled in both dimensions to keep the aspect ratio. // That works surprisingly well for alert icons. Charts can be made shorter or taller. // If you do not have enough room to display the entire width, you should cut off // pixels on the left. private class UniqueStack { // This is like a stack because you the last thing you push is the first thing you pop. // But duplicates are not allowed. If you push an item that is already in the here, // the item moves to the top of the stack. You can also remove an item efficiently. // All operations are O(1); private LinkedList _asStack = new LinkedList(); private Dictionary> _asMap = new Dictionary>(); public bool Contains(T item) { return _asMap.ContainsKey(item); } public void Remove(T item) { LinkedListNode node; if (_asMap.TryGetValue(item, out node)) { _asMap.Remove(item); _asStack.Remove(node); } // Else, do nothing. } public bool Empty() { return _asMap.Count == 0; } public T Pop() { if (Empty()) return default(T); T result = _asStack.First.Value; Remove(result); return result; } public void Push(T item) { Remove(item); _asMap[item] = _asStack.AddFirst(item); } }; private enum Status { New, LocalQueue, SentToServer, Ready }; private class ImageHolder { public string Location { get; private set; } public string FileName { get; private set; } public Bitmap Bitmap { get; private set; } public Status Status { get; set; } //public DateTime LastAccessTime { get; set; } private readonly ImageCacheManager _imageCacheManager; public ImageHolder(ImageCacheManager imageCacheManager, string location, string fileName) { _imageCacheManager = imageCacheManager; Location = location; FileName = fileName; } public void SendNow() { // Watch for reentrant calls. Debug.Assert(Status == Status.LocalQueue); Status = Status.SentToServer; _imageCacheManager._sendManager.SendMessage(TalkWithServer.CreateMessage( "command", "download", "location", Location, "file", FileName), Response); } private void Response(byte[] body, object unused) { // Watch for reentrant calls! lock (_imageCacheManager._mutex) _imageCacheManager._requestsAvailable++; if (null == body) { // Communications failure. Try again. Status = Status.New; _imageCacheManager.ReAdd(Location, FileName); } else { Status = Status.Ready; try { MemoryStream stream = new MemoryStream(body); Bitmap = new Bitmap(stream); } catch { // Invalid bitmap image. Most likely we requested something that // doesn't exist so the server sent the empty string. But the // server could send us anything. No retry. } _imageCacheManager.ReportSoon(); _imageCacheManager.CheckForWork(); } } /* public override bool Equals(object obj) { return Equals(obj as ImageHolder); } public bool Equals(ImageHolder other) { if ((object)other == null) return false; return (Folder == other.Folder) && (FileName == other.FileName); } public static bool operator ==(ImageHolder a, ImageHolder b) { // If both are null, or both are same instance, return true. if (System.Object.ReferenceEquals(a, b)) { return true; } // If the first is null, return false. if ((object)a == null) { return false; } // Return true if the fields match: return a.Equals(b); } public override int GetHashCode() { return FileName.GetHashCode() ^ Folder.GetHashCode(); } */ } private UniqueStack _normal = new UniqueStack(); private UniqueStack _lowPriority = new UniqueStack(); struct Key { public string Location; public string FileName; } private Dictionary _all = new Dictionary(); private int _requestsAvailable = 50; private object _mutex = new object(); public event CachedImageAvailable CachedImageAvailable; public Bitmap GetAlert(string internalCode, bool lowPriority = false) { return Get(ALERTS, internalCode + ".gif", lowPriority); } public Bitmap GetFilter(string internalCode, bool lowPriority = false) { return Get(FILTERS, internalCode + ".gif", lowPriority); } public Bitmap Get(string location, string fileName, bool lowPriority = false, bool setTime = true) { ImageHolder result; Key key; key.FileName = fileName; key.Location = location; lock (_mutex) { if (!_all.TryGetValue(key, out result)) { // First request for this image. Otherwise, we'd reuse the previous request. result = new ImageHolder(this, location, fileName); _all.Add(key, result); } if ((result.Status == Status.New) || (result.Status == Status.LocalQueue)) { result.Status = Status.LocalQueue; if (lowPriority) { if (!_normal.Contains(result)) _lowPriority.Push(result); } else { _normal.Push(result); _lowPriority.Remove(result); } CheckForWork(); //if (setTime) // result.LastAccessTime = DateTime.Now; } } return result.Bitmap; } private void ReAdd(string location, string fileName) { Get(location, fileName, setTime: false); } private void CheckForWork() { // Warning: There could be reentrant calls to this method if we there is // a failure in the send call. SendManager should protect us from an // infinite recursion. lock (_mutex) { while (_requestsAvailable > 0) { if (!_normal.Empty()) SendNow(_normal.Pop()); else if (!_lowPriority.Empty()) SendNow(_lowPriority.Pop()); else return; } } } private void SendNow(ImageHolder imageHolder) { // Watch for reentrant calls. _requestsAvailable--; Debug.Assert(imageHolder.Status == Status.LocalQueue); imageHolder.SendNow(); } private readonly ISendManager _sendManager; internal ImageCacheManager(ISendManager sendManager) { _sendManager = sendManager; } private volatile bool _reportIsPending = false; private void ReportSoon() { // Try to consolidate the reports. If we send a lot of requests at once // we would expect to get a lot of responses close together. So we could // get better performance if we grouped them all together into one // callback. Also, it's nice to call the callback from another thread // to be sure we are not holding any locks and we do not have any concerns // about reentrant code. lock (_mutex) { if (!_reportIsPending) { _reportIsPending = true; ThreadPool.QueueUserWorkItem(new WaitCallback(ReportSoonCallback)); } } } private void ReportSoonCallback(object unused) { Thread.Sleep(0); lock (_mutex) { _reportIsPending = false; } CachedImageAvailable callback = CachedImageAvailable; if (null != callback) callback(); } } }