using System; using System.Collections.Generic; using System.Drawing; using System.Text; using System.Xml; using TradeIdeas.TIProData.Configuration; using TradeIdeas.XML; namespace TradeIdeas.TIProData { /// /// This is part of the metadata that comes with the alerts and top lists. /// The server is responsible for parsing a request. It might change your request. /// The server sends metadata back to say what columns it plans to provide. /// Some features of the ColumnInfo class are aimed specifically at a GUI. /// However, even if you do not have a GUI, you need to use this metadata /// to decode the data. /// public class ColumnInfo: IDisplayColumn { /// /// This compares two lists of comlumns to see if they are equal. We can /// receive this metadata for different reasons. We should receive an update /// any time the columns actually change. But we might receive the list /// of columns at other times. The GUI often does not want to update unless /// something really changed. /// /// First list of columns. /// Second list of columns. /// True if the two objects are the same. public static bool Equal(IList a, IList b) { // It seems like there should be an easier way to do this! // Something built into dot net should do this. if (Object.ReferenceEquals(a, b)) // Same object or both null. return true; if ((null == a) || (null == b)) // Exactly one is null. return false; if (a.Count != b.Count) return false; for (int i = 0; i < a.Count; i++) if (a[i] != b[i]) return false; return true; } /// /// This is what it's called in the the XML messages. This code is specific to this one request. /// A typical value might be "c0" for the first column. /// public string WireName { get; set; } /// /// Like BidSize or Price. Use this to find the icon, help, etc. /// public string InternalCode { get; set; } /// /// False means it's a filter and has an icon. True means we display text on a simple background. /// public bool TextHeader { get; set; } private string _description = ""; /// /// Human readable, like "Bid Size" /// public string Description { get { if (_description == "") _description = FindField("DESCRIPTION"); return _description; } set { _description = value; } } /// /// Human readable. /// public string Units { get { return FindField("UNITS"); } } // See CommonConfig.php public string Format { get; set; } public string Graphics { get; set; } /// /// We expect the value in this column to be a number (like Price or Yesterday's Volume), /// rather than a string (like Company Name or Sector). Some people used TextHeader to /// guess this information, but this function is more reliable. /// /// True for numeric, False for string. public bool IsNumeric() { switch (Format) { case "p": case "0": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": return true; default: return false; } } /// /// In pixels. (Of course this depends on what font you are using!) /// public int PreferredWidth { get; set; } /// /// False means column text will not word wrap. /// public bool WrapMode { get; set; } /// /// This string will be measured to determine the preferred column size /// public string SizeHint { get; set; } private bool _defaultVisible = true; public bool DefaultVisible { get { return _defaultVisible; } set { _defaultVisible = value; } } /// /// If defined, this function is called with the string value of a cell to /// determine the cell's background color. /// public Func CustomBackgroundColor { get; set; } public GradientInfo GradientInfo { get; set; } /// /// This is used for coloring individual cells background using a gradient. /// public double? MinForColor { get; set; } /// /// This is used for coloring individual cells background using a gradient. /// public double? MidForColor { get; set; } /// /// This is used for coloring individual cells background using a gradient. /// public double? MaxForColor { get; set; } private bool _useEmptyColor = true; /// /// Controls whether a blank cell in a gradient colored column shows the "empty" background color (currently gray). /// public bool UseEmptyColor { get { return _useEmptyColor; } set { _useEmptyColor = value; } } private bool _realTimeUpdates = false; /// /// Controls whether this column should be refreshed when a row is updated. /// public bool RealTimeUpdates { get { return _realTimeUpdates; } set { _realTimeUpdates = value; } } private bool _hideProfitDigits = false; /// /// Controls whether this column should hide the decimal component of columns with the "profit" format. /// public bool HideProfitDigits { get { return _hideProfitDigits; } set { _hideProfitDigits = value; } } private XmlNode _xml; private string FindField(string xmlName) { // I worry a little bit about the performance of this. return _xml.PropertyForCulture(xmlName); } /// /// In pixels. (Of course this depends on what font you are using!) /// public int MinimumWidth { get; set; } /// /// Fill this column with extra space to make grid 100% width /// public bool FillMode { get; set; } /// /// In pixels. (Of course this depends on what font you are using!) /// public int MaximumWidth { get; set; } public ColumnInfo() { } /// /// Creates a new column from data from the server. This will never throw an exception. /// We try to fill in blanks and invalid data with reasonable defaults. /// /// The description from the server. /// The form type. // add parameter for form type - RVH20210602 // ColumnInfo(List columnXml) : // this(columnXml.Select()) public ColumnInfo(List columnXml, string formType) : this(columnXml.Select(), formType) { // The way we use the XML, we might as well call Select() here. It's // a slight performance increase. } /// /// Creates a new column from data from the server. This will never throw an exception. /// We try to fill in blanks and invalid data with reasonable defaults. /// /// The description from the server. /// The form type. // add parameter for form type - RVH20210602 //public ColumnInfo(XmlNode columnXml) public ColumnInfo(XmlNode columnXml, string formType) { WireName = columnXml.SafeName(); InternalCode = columnXml.Property("CODE"); TextHeader = columnXml.Property("TEXT_HEADER") == "1"; Format = columnXml.Property("FORMAT"); Graphics = columnXml.Property("GRAPHICS"); DefaultVisible = columnXml.Property("DEFAULT_VISIBLE", true); RealTimeUpdates = columnXml.Property("REAL_TIME_UPDATES", false); string sizeHint = columnXml.Property("SIZE_HINT", "-1"); if (sizeHint != "-1") SizeHint = sizeHint; double minForColor = columnXml.Property("MIN_FOR_COLOR", double.PositiveInfinity); if (minForColor != double.PositiveInfinity) MinForColor = minForColor; double midForColor = columnXml.Property("MID_FOR_COLOR", double.PositiveInfinity); if (midForColor != double.PositiveInfinity) MidForColor = midForColor; double maxForColor = columnXml.Property("MAX_FOR_COLOR", double.PositiveInfinity); if (maxForColor != double.PositiveInfinity) MaxForColor = maxForColor; if (MinForColor.HasValue && MinForColor.Value != double.PositiveInfinity && MidForColor.HasValue && MidForColor.Value != double.PositiveInfinity && MaxForColor.HasValue && MaxForColor.Value != double.PositiveInfinity) { this.GradientInfo = new GradientInfo(); this.GradientInfo.Add(MinForColor.Value, Color.Red); this.GradientInfo.Add(MidForColor.Value, Color.Black); this.GradientInfo.Add(MaxForColor.Value, Color.Green); } _xml = columnXml; PreferredWidth = GetPreferredWidth(); WrapMode = GetPreferredWrapMode(); // TODO: This should come from the server or defined in Common.xml! // Set MinimumWidth - RVH20210525 MinimumWidth = columnXml.Property("MINIMUMWIDTH", PreferredWidth); // get column properties from global settings - RVH20210528 DataGridColumnSetting dataGridColumnSetting = GlobalDataSettings.GetDataGridColumnSetting(formType, InternalCode); if (dataGridColumnSetting != null) { // only set the PreferredWidth if greater than zero - RVH20210623 if (dataGridColumnSetting.PreferredWidth > 0) PreferredWidth = dataGridColumnSetting.PreferredWidth; // only set the MinimumWidth if greater than zero - RVH20210623 if (dataGridColumnSetting.MinimumWidth > 0) MinimumWidth = dataGridColumnSetting.MinimumWidth; // only set the Format if not null - RVH20210616 if (dataGridColumnSetting.Format != null) Format = dataGridColumnSetting.Format; // only set the Description if DisplayName is not null - RVH202116 if (dataGridColumnSetting.DisplayName != null) Description = dataGridColumnSetting.DisplayName; FillMode = dataGridColumnSetting.FillMode; MaximumWidth = dataGridColumnSetting.MaximumWidth; } } private int GetPreferredWidth() { // TODO: This should depend on the current font. Look for "MeasureString" in ConfigWindow.cs // for an idea of where to start. Note that the title strings will be moved to the XML config // file, if they are not there already, so they can be localized. Any solution should be // applicable to the top list. Even though it currently ignores the header when setting the // width, it shouldn't. switch (InternalCode) { case "D_Desc": return 175; case "D_Sector": return 175; case "D_SubSector": return 175; case "D_IndGrp": return 175; case "D_Industry": return 175; case "D_SubIndustry": return 175; case "D_Name": return 175; case "D_Exch": return 105; case "D_Type": return 41; case "D_Time": return 80; case "D_Symbol": return 71; case "D_Quality": return 50; default: return 40; } } private bool GetPreferredWrapMode() { // TODO: This should come from the server or defined in Common.xml! switch (InternalCode) { case "D_Desc": return true; case "D_Sector": return true; case "D_SubSector": return true; case "D_IndGrp": return true; case "D_Industry": return true; case "D_SubIndustry": return true; case "D_Name": return true; case "D_Exch": return true; case "Notes": return true; case "CompanyName": return true; case "D_Type": return false; case "D_Time": return false; case "D_Symbol": return false; case "D_Quality": return false; default: return false; } } /// /// This is aimed at various test programs. This allows a programmer to easily see the metadata. /// /// Output is appended to here. public void DebugString(StringBuilder result) { result.Append("Column(WireName=\""); result.Append(WireName); result.Append("\", InternalCode=\""); result.Append(InternalCode); result.Append("\", Description=\""); result.Append(Description); result.Append("\", Units=\""); result.Append(Units); result.Append("\", Format=\""); result.Append(Format); result.Append("\")"); } /// /// This returns a description of the object aimed at a developer. /// This uses the same format as DebugString(). /// /// The description. public override string ToString() { StringBuilder result = new StringBuilder(); DebugString(result); return result.ToString(); } /// /// This is often displayed in a GUI as a tooltip. This is aimed at the end user. /// /// Something like "Price ($)" public string LongDescription() { if (Units == "") return Description; else return Description + " (" + Units + ")"; } /// /// This follows follows the C# conventions for read only objects. /// Two objects are considered equal if they are of the same type and /// each of their fields is equal. /// /// Right hand side. /// True if and only if the objects have identical data. public bool Equals(ColumnInfo other) { if ((object)other == null) return false; return (WireName == other.WireName) && (InternalCode == other.InternalCode) /*&& (TextHeader == other.TextHeader)*/ && (Description == other.Description) && (Format == other.Format) && (Graphics == other.Graphics) && (Units == other.Units); // The text header thing was causing a lot of problems. // Every time I got a new config from the server, this said it was different. // Things like Symbol which were always text were fine. // Price showed up as different between the server and the client. } /// /// This follows follows the C# conventions for read only objects. /// Two objects are considered equal if they are of the same type and /// each of their fields is equal. /// /// Right hand side. /// True if and only if the objects have identical data. public override bool Equals(Object obj) { return Equals(obj as ColumnInfo); } /// /// This follows follows the C# conventions for read only objects. /// Two objects are considered equal if they are of the same type and /// each of their fields is equal. /// /// Left hand side. /// Right hand side. /// True if and only if the objects have identical data. public static bool operator ==(ColumnInfo a, ColumnInfo b) { // If both are null, or both are same instance, return true. if (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); } /// /// This follows follows the C# conventions for read only objects. /// Two objects are considered equal if they are of the same type and /// each of their fields is equal. /// /// Left hand side. /// Right hand side. /// True if and only if the objects are NOT equal. public static bool operator !=(ColumnInfo a, ColumnInfo b) { return !(a == b); } /// /// This hash code logic is consistent with our implementation of Equals(). /// /// The hash code. public override int GetHashCode() { return InternalCode.GetHashCode(); } }// This allows you to color the output based on a value. You specify the colors // for several values. If the given value exactly matches one of the values // stored in this object, the corresponding color is returned. Otherwise, we // look for the nearst values slightly above and below the given value, and pick // a color in between those two colors. public class GradientInfo { public struct ColorForPoint { public Color Below; public Color At; public Color Above; // This is the same problem we had in AlertColorPicker.cs. We want two colors to be // identical if they are rendered on the screen the same way. We want Black and // #000000 to be considered identical. public override bool Equals(object obj) { if (obj is ColorForPoint) return Equals((ColorForPoint)obj); else return false; } public bool Equals(ColorForPoint other) { return (Below.ToArgb() == other.Below.ToArgb()) && (At.ToArgb() == other.At.ToArgb()) && (Above.ToArgb() == other.Above.ToArgb()); } public override int GetHashCode() { // Assume colors might be swapped a lot, so xor without the shift would not have been // a good hash code. Assume no transparency. We don't enforce that, and nothing will // fail. But the hash code might not be as good. return Below.ToArgb() ^ (At.ToArgb() << 3) ^ (Above.ToArgb() << 6); } } //Made the Map public...using this within the TopListColorChooser.cs public SortedList Map { get; set; } public GradientInfo() { Map = new SortedList(); } private const string VALUE = "VALUE"; private const string BELOW = "BELOW"; private const string AT = "AT"; private const string ABOVE = "ABOVE"; public GradientInfo(XmlNode source) : this() { foreach (XmlElement rowNode in source.Enum()) { double value = rowNode.Property(VALUE, Double.NaN); if (Double.IsNaN(value)) continue; Color at = rowNode.Property(AT, XmlHelper.InvalidColor); if (at == XmlHelper.InvalidColor) continue; ColorForPoint colors; colors.At = at; colors.Above = rowNode.Property(ABOVE, at); colors.Below = rowNode.Property(BELOW, at); Add(value, colors); } } public void Save(XmlNode node) { foreach (KeyValuePair kvp in Map) { XmlNode rowNode = node.NewNode("ROW"); rowNode.SetProperty(VALUE, kvp.Key); rowNode.SetProperty(AT, kvp.Value.At); if (kvp.Value.Below != kvp.Value.At) rowNode.SetProperty(BELOW, kvp.Value.Below); if (kvp.Value.Above != kvp.Value.At) rowNode.SetProperty(ABOVE, kvp.Value.Above); } } public void SaveAs(XmlNode parent, string name) { if (!Empty()) Save(parent.NewNode(name)); } public void Add(double value, ColorForPoint colors) { if (Map.ContainsKey(value)) Map.Remove(value); Map.Add(value, colors); } public void Add(double value, Color color) { ColorForPoint colors; colors.Above = color; colors.At = color; colors.Below = color; Add(value, colors); } public void Add(double value, Color Below, Color At, Color Above) { ColorForPoint colors; colors.Above = Above; colors.At = At; colors.Below = Below; Add(value, colors); } public void Remove(double value) { Map.Remove(value); } private static byte Between(int a, int b, double aWeight) { int originalA = a; int originalB = b; a = 0xff & a; b = 0xff & b; return (byte)(a * aWeight + b * (1.0 - aWeight)); } private static Color Between(Color a, Color b, double aWeight) { int argb = a.ToArgb(); int brgb = b.ToArgb(); return Color.FromArgb(Between(argb >> 16, brgb >> 16, aWeight), Between(argb >> 8, brgb >> 8, aWeight), Between(argb, brgb, aWeight)); } public bool Empty() { return Map.Count == 0; } public Color GetColor(double value) { if (Empty()) throw new ArgumentException("No color data."); /* // This seems reasonable, but the cast is not gaurenteed to work. int index = (Map.Keys as List).BinarySearch(value); if (index >= 0) // Found an exact match! return Map.Values[index].At; int indexOfLarger = ~index; if (indexOfLarger == 0) // value is below the lowest item in our list. return Map.Values[0].Below; if (indexOfLarger > Map.Count) // value is above the highest item in our list. return Map.Values[Map.Count - 1].Above; Color lowerColor = Map.Values[indexOfLarger - 1].Above; Color higherColor = Map.Values[indexOfLarger].Below; double lowerValue = Map.Keys[indexOfLarger - 1]; double higherValue = Map.Keys[indexOfLarger]; return Between(lowerColor, higherColor, (higherValue - value) / (higherValue - lowerValue)); */ // After a lot of googling, I see no simple way to use a binary search to get to the correct // value more quickly. The best answer seems to be to write my own binary search for ilist. // for now I'm assuming the list will be short so it doesn't matter much. :( if (value < Map.Keys[0]) // The given value is lower than the lowest item in our map. return Map.Values[0].Below; int index = Map.IndexOfKey(value); if (index > -1) // An exact match! return Map.Values[index].At; bool first = true; KeyValuePair previous = new KeyValuePair(); foreach (KeyValuePair kvp in Map) { if (first) // This shouldn't really be necessary. We already checked that the // given value was not less than the first item in the list. first = false; else { if (value < kvp.Key) // The given value is between the current item and the previous item. return Between(previous.Value.Above, kvp.Value.Below, (kvp.Key - value) / (kvp.Key - previous.Key)); } previous = kvp; } // The given value is higher than the highest item in our map. return Map.Values[Map.Count - 1].Above; } private const int RMS_CUTOFF = 288; private static double RMSBrightness(Color color) { // Color.GetBrightness() gives inconsistent results. This formula // does a better job comparing colors. int argb = color.ToArgb(); int r = (argb >> 16) & 0xff; int g = (argb >> 8) & 0xff; int b = argb & 0xff; return Math.Sqrt(r * r + g * g + b * b); } private const double GREEN_ADJ = 255.0 / 201.0; // green at 201 is just as bright as blue at 255; private const double RED_ADJ = 255.0 / 218.0; // red at 218 is just as bright as blue at 255; private const int RMSA_CUTOFF = 269; // If the background is brighter than this, use black for the foreground. private static double RMSABrightness(Color color) { // RMSBrightness is close, but it assumes that (255, 0, 0), (0, 255, 0) and (0, 0, 255) are // all just as bright. int argb = color.ToArgb(); int r = (argb >> 16) & 0xff; int g = (argb >> 8) & 0xff; int b = argb & 0xff; return Math.Sqrt(r * r * RED_ADJ * RED_ADJ + g * g * GREEN_ADJ * GREEN_ADJ + b * b); } /// /// Returns Color.Black or Color.White depending on the darkness of the passed color. Useful for choosing a forecolor that makes sense given /// the darkness of a background color. See also /// /// /// public static Color AltColor(Color value) { return IsDark(value) ? Color.Black : Color.White; } /// /// Returns true if the passed color is sufficiently "dark". Useful when the user can pick any color for the background or a gradient /// and we need to determine whether to use a light or dark color for the foreground. See also /// /// /// Default is 269 which seems to work well in most cases. /// public static bool IsDark(Color color, int? cutoff = null) { if (!cutoff.HasValue) cutoff = RMSA_CUTOFF; return RMSABrightness(color) > cutoff.Value; } public GradientInfo DeepCopy() { GradientInfo result = new GradientInfo(); foreach (KeyValuePair kvp in Map) result.Add(kvp.Key, kvp.Value); return result; } // I'm comparing these field by field. That's inconsistent with the normal // C# way of doing things because this object is not immutable. It is up to // the user to make a deep copy at appropriate times. Be sure to make a // deep copy before adding something to a hash table, or things will not // work as you expect. // // The idea is that you will have a list of recently used settings. If you // modify a setting and then modify it back, it should still be considered // a duplicate. We don't want duplicates in the table. As soon as the // user might modify a setting, we need to make a deep copy. So even if // he doesn't change anything, we will still be looking at a different object. // so we can't use object identity to look for duplicates. public bool Equals(GradientInfo other) { if ((object)other == null) return false; if (Map.Count != other.Map.Count) return false; for (int i = 0; i < Map.Count; i++) { if (Map.Keys[i] != other.Map.Keys[i]) return false; if (!Map.Values[i].Equals(other.Map.Values[i])) return false; } return true; } public override bool Equals(Object obj) { return Equals(obj as GradientInfo); } public static bool operator ==(GradientInfo a, GradientInfo b) { // If both are null, or both are same instance, return true. if (Object.ReferenceEquals(a, b)) { return true; } // If the first is null, return false. if ((object)a == null) { return false; } // Return true if the contents match: return a.Equals(b); } public static bool operator !=(GradientInfo a, GradientInfo b) { return !(a == b); } public override int GetHashCode() { // This somewhat ineffecient. But it should work. int result = Map.Count; foreach (KeyValuePair kvp in Map) { result += kvp.Key.GetHashCode(); result -= kvp.Value.GetHashCode(); result *= 11; } return result; } } }