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;
}
}
}