using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Xml; using System.Windows.Forms.DataVisualization.Charting; using TradeIdeas.MiscSupport; using TradeIdeas.TIQ.GUI; using TradeIdeas.TIQData; using TradeIdeas.ServerConnection; using TradeIdeas.XML; using System.IO; using LumenWorks.Framework.IO.Csv; // TODO I disabled the old code but haven't added the new code. // This doesn't listen for results yet. namespace TradeIdeas.TIQ { public struct ColumnDataPoint { public int column; //prototype Editor public double point; public string startTime; } public partial class TIQChart : Form, ISaveLayout { private readonly ConnectionMaster _connectionMaster; private const string FORM_TYPE = "TIQ_CHART"; private SingleGridRequest _request; private string _fileNameSaved; private int _tabCount = 0; private int selectedRwIndex = -1; //specifically for when one clicks on cell of CsvPreview. If this value = -1, then we're not using the CsvPreview window... private string seriesEditorNameCsvPreview;// when one is using the CsvPreview. //for saving/restoring combobox indices of tab pages private int high; private int low; private int open; private int close; //The below will be a "list of lists" //Each nested list corresponds to each column depicted in the prototype editor //The nested list contains the data values that come off of the server which //correspond to that column private List> _dataList = new List>(); public TIQChart() { _connectionMaster = GUI.Environment.DefaultConnectionMaster; InitializeComponent(); prototypeEditor1.ColumnHeadingsChanged += prototypeEditor1_ColumnHeadingsChanged; addNewSeries(); } static public void RegisterLayout() { LayoutManager.Instance().AddRestoreRule(FORM_TYPE, (RestoreLayout)delegate(XmlNode description, bool ignorePosition, bool cascadePosition) { TIQChart form = new TIQChart(); LayoutManager.RestoreBase(description, form, ignorePosition, cascadePosition); form.prototypeEditor1.GetContents(description); form.GetContents(description); }); } void ISaveLayout.SaveLayout(XmlNode parent) { XmlNode description = LayoutManager.SaveBase(parent, this, FORM_TYPE); prototypeEditor1.SetContents(description); SetContents(description); } public void SetContents(XmlNode destination) { XmlNode tabSheet = destination.NewNode("TABSHEET"); XmlNode symbolBox = destination.NewNode("SYMBOL_TEXTBOX"); symbolBox.SetProperty("SYMBOL", txtStockSymbol.Text); foreach (TabPage tab in seriesEditorTabControl.TabPages) { SeriesEditor sEdit = (SeriesEditor)tab.Controls[0]; XmlNode sEditor = tabSheet.NewNode("TAB"); sEdit.SetContents(sEditor); } } public void GetContents(XmlNode description) { seriesEditorTabControl.TabPages.Clear(); chart1.Series.Clear(); XmlNode symbolBox = description.Node("SYMBOL_TEXTBOX"); txtStockSymbol.Text = symbolBox.Property("SYMBOL"); XmlNode tabSheet = description.Node("TABSHEET"); foreach (XmlNode node in tabSheet.Enum()) { //Prepare the new tab page... TabPage tabPage = new TabPage(); tabPage.BackColor = Color.White; //add the series editor control to the tab page... SeriesEditor s = new SeriesEditor(); List columnHeadings = prototypeEditor1.GetColumnHeadings(); foreach (object column in columnHeadings) { addToSeriesEditorCombos(s, column); } Series newSeries = new Series(); XmlNode seriesEditor = node.Node("SERIES_EDITOR"); s.TabPage = tabPage; s.WorkingChart = chart1; tabPage.Text = seriesEditor.Property("SERIES_NAME"); chart1.Series.Add(newSeries); s.TabSeries(ref newSeries); tabPage.Controls.Add(s); s.GetContents(seriesEditor); s.Dock = DockStyle.Fill; //Add the tab page. seriesEditorTabControl.TabPages.Add(tabPage); } foreach (TabPage t in seriesEditorTabControl.TabPages) { //When the tabControl has been loaded completely with it's tab pages, //we must now loop through these pages and set the WorkingTabSheet property //of that tab page's SeriesEditor control. foreach (Control c in t.Controls) { if (c as SeriesEditor != null) { SeriesEditor test = (SeriesEditor)c; test.WorkingTabSheet = seriesEditorTabControl; test.setLegendTextBox(test.TabPage.Text); } } } } private void requestData() { if (null != _request) _request.Abort(); _request = new SingleGridRequest(txtStockSymbol.Text, prototypeEditor1.GetTcl(), false, _connectionMaster.SendManager, this, delegate(SingleGridRequest.Response response) { gatherChartData(response.csvData); }); } private void requestDataButton_Click(object sender, EventArgs e) { selectedRwIndex = -1; requestData(); } /// /// This class requests a grid from the server. This takes care of a lot of details, like automatic /// retries. The public interface to this class should only be called from the GUI thread, and the /// callback will return in the GUI thread. /// private class SingleGridRequest { public struct Response { public String symbol; public String csvData; }; public readonly String Symbol; private Dictionary _message; private Control _owner; private SendManager _sendManager; private Action _callBack; /// /// Send a request to the server. /// /// Request data for this symbol. /// Probably the output of PrototypeEditor.GetTcl(). /// /// If a formula returns an error and this is true we print an error message. If it's /// false we display "". False is better for things aimed at an end user. The error /// messages are crude and really only helpful to a programmer. /// /// Use this to send the request. /// /// Use this to move server responses into the correct thread. Also, if this is a /// form and the form closes, the callback and any automatic retries are /// automatically canceled. If this is null, everything will still work except for /// the automatic cancel. /// /// /// This will be called when the data comes from the server. This will only be called /// once. This will be called in the GUI thread. /// public SingleGridRequest(String symbol, String prototype, Boolean showErrors, SendManager sendManager, Control owner, Action callBack) { Symbol = symbol; _owner = owner ?? new Control(); _sendManager = sendManager; _callBack = callBack; StringBuilder symbols = new StringBuilder(); symbols.LAppend(symbol); object[] message = new object[] { "command", "flex_command", "subcommand", "make_and_show", "prototype", prototype, "symbols", symbols.ToString(), "show_errors", showErrors?"1":null }; _message = TalkWithServer.CreateMessage(message); _sendManager.SendMessage(_message, GetDataResponse); } // Cancel any callbacks. Unlike the TalkWithServer messages, this takes // effect immediately. public void Abort() { _callBack = null; } private void GetDataResponse(byte[] body, object clientId) { _owner.BeginInvokeIfRequired((MethodInvoker)delegate { ResponseInThread(body); }); } private void ResponseInThread(byte[] body) { if (null == _callBack) // Aborted return; if (null == body) // Communication problem. Try again. _sendManager.SendMessage(_message, GetDataResponse); else { Response response; XmlNode message = XmlHelper.Get(body).Node(0); String type = message.Property("type"); if (type == "result") response.csvData = message.Text(); else // Error reported by server. response.csvData = ""; response.symbol = Symbol; _callBack(response); } } } private void gatherChartData(string body) { _dataList.Clear(); chart1.Annotations.Clear(); List columnHeadings = prototypeEditor1.GetColumnHeadings(); TextReader textReader = new StringReader(body); CsvReader csvRead = new CsvReader(textReader, true); csvRead.MissingFieldAction = MissingFieldAction.ReplaceByEmpty; /*With the exception of the start and endTime data, the other headers correspond with the rows shown in the prototype editor. So we will make "nested" lists to holds that data The indices of these nested lists (within the containing list "_dataList") correspond to the indices of the columns in the Prototype editor, which, of course, are also the same as the indices seen in the comboboxes in the SeriesEditor as well */ int numberOfList = columnHeadings.Count; for (int i = 0; i < numberOfList; i++) { _dataList.Add(new List()); } Dictionary serverEditorMapping = new Dictionary(); List headingNames = new List(); List missingPrototypeData = new List(); //holds indices of _dataList which would have empty points(e.g. server may be missing a column you've indicated in the prototype editor).. foreach(PrototypeEditor.ColumnHeading heading in columnHeadings) { headingNames.Add(heading.ToString()); } List headersFromServer = new List(csvRead.GetFieldHeaders()); //contains the column header names List temp = new List(headersFromServer); //Now we'll go through each string in headingNames( of which the indices correspond to those in _dataList) and //see whether it matches with any field name in the headersFromServer List. If there's a match, we //populate the dictionary with the index of that name from headersFromServer List as key, with the index of the match within //headingName List as value.. for (int i = 0; i < numberOfList; i++) { string test = headingNames[i]; if (temp.Contains(headingNames[i])) { int j = headersFromServer.IndexOf(headingNames[i]); serverEditorMapping.Add(j, i); temp.Remove(headingNames[i]); } else //if our column header (combobox item) isn't in the list of those produced by server, there's a "hole" in data.. { missingPrototypeData.Add(i); } } while (csvRead.ReadNextRecord()) { int headerTally = 0; //zeroth column (far left side) in the Prototype editor string sDate = ""; for (int i = 0; i < csvRead.FieldCount; i++) { if (i == 0 || i == 1) //These are the start and end time data. That will be a future TODO { if (i == 0) { sDate = csvRead[i]; } headerTally++; continue; } else { ColumnDataPoint line = new ColumnDataPoint(); line.column = headerTally; double result; if (Double.TryParse(csvRead[i], out result) == false) { /*Here is where we're tagging erroneous points as empty. Before such points are added to the chart, they're checked to see whether they're NaN. If so, then that value is set to zero. It is then added to the chart, then tagged as "empty" and will not show up on the chart display.*/ result = double.NaN; } line.point = result; line.startTime = sDate; //now get the corresponding value for datalist index using the current index "headerTally" (which corresponds to headersFromServer) if (serverEditorMapping.ContainsKey(headerTally)) { _dataList[serverEditorMapping[headerTally]].Add(line); } else { /*TODO If the headerTally is not in the serverEditorMapping dictionary, * it means that it wasn't a grid column. The server can send extra * stuff that wasn't asked for. And currently since neither the grid column * headers nor comboboxes have any such extra stuff listed because it wasn't requested..it's not viewable. * Theoretically you can also check for the extra "columns" before going into the while (csvRead.ReadNextRecord()) * by a) looping through the "temp" List after having removed those which are your grid columns-any remaining string that's neither "start time" nor "end time" * would be an additial goodie delivered by server. Then you could get the index of that item from our headersFromServer List. */ } } headerTally++; } fillDataHolesWithEmpty(missingPrototypeData,sDate); } UpdateChart(); } private void fillDataHolesWithEmpty(List emptyListsNos,string sDate) { //Here we are filling the "holes" with empty points. These indices // of the "holes" correspond with the indices of the of the comboboxes //as well as those of the column headers in the prototypeEditor grid. foreach (int i in emptyListsNos) { List dat = _dataList[i]; ColumnDataPoint line = new ColumnDataPoint(); line.column = i; line.point = double.NaN; line.startTime = sDate; dat.Add(line); } } private void UpdateChart() { //Here, we're updating the chart of all of the series after having //processed the newly arrived data... chart1.Annotations.Clear(); foreach (TabPage tab in seriesEditorTabControl.TabPages) { SeriesEditor sEdit = (SeriesEditor)tab.Controls[0]; //There's only one control in tab sheet- it's the SeriesEditor control sEdit.UpdateChart(_dataList, true); } if (selectedRwIndex != -1) { addAnnotationToPoint(); } } private void prototypeEditor1_ColumnHeadingsChanged(PrototypeEditor obj, bool onlyLookAtNames) { UpdateColumnNames(); } private void UpdateColumnNames() { ///*TODO-If there are more than one tab in tabsheet, // the non-selected tabs will default to only the first columnHeading*/ List columnHeadings = prototypeEditor1.GetColumnHeadings(); foreach (TabPage tab in seriesEditorTabControl.TabPages) { updateSeriesComboBoxes(tab, columnHeadings); } } private void updateSeriesComboBoxes(TabPage tab, List columnHeadings) { SeriesEditor sEdit = (SeriesEditor)tab.Controls[0]; //There's only one control in tab sheet- it's the SeriesEditor control saveSeriesComboIndices(sEdit); clearSeriesEditorCombos(sEdit); foreach (object column in columnHeadings) { addToSeriesEditorCombos(sEdit, column); } restoreSeriesComboIndices(sEdit); } private void clearSeriesEditorCombos(SeriesEditor seriesCombo) { seriesCombo.comboClose.Items.Clear(); seriesCombo.comboOpen.Items.Clear(); seriesCombo.comboHigh.Items.Clear(); seriesCombo.comboLow.Items.Clear(); } private void addToSeriesEditorCombos(SeriesEditor seriesCombo, object column) { seriesCombo.comboClose.Items.Add(column); seriesCombo.comboOpen.Items.Add(column); seriesCombo.comboHigh.Items.Add(column); seriesCombo.comboLow.Items.Add(column); } private void saveSeriesComboIndices(SeriesEditor sEdit) { /*this method (along with the restore method below) allows the comboboxes of the series editor to retain their chosen indices when a new column is added(or its name changed). When a user has many series(tab pages), and he decides to add another column to the prototype editor, the indices of the Tab pages' comboboxes will not change-even though the new column is added to the comboboxes.*/ /*if any of the comboboxes index is -1 (nothing showing), all of them will be -1.This occurs when a new series (tab page) is freshly added*/ if (sEdit.comboHigh.SelectedIndex == -1) { high = low = open = close = 0; } else { high = sEdit.comboHigh.SelectedIndex; low = sEdit.comboLow.SelectedIndex; open = sEdit.comboOpen.SelectedIndex; close = sEdit.comboClose.SelectedIndex; } } private void restoreSeriesComboIndices(SeriesEditor sEdit) { if (sEdit.comboHigh.Items.Count != 0) { sEdit.comboHigh.SelectedIndex = isSelectedIndexSafe(sEdit.comboHigh.Items.Count, high); sEdit.comboLow.SelectedIndex = isSelectedIndexSafe(sEdit.comboLow.Items.Count, low); sEdit.comboOpen.SelectedIndex = isSelectedIndexSafe(sEdit.comboOpen.Items.Count, open); sEdit.comboClose.SelectedIndex = isSelectedIndexSafe(sEdit.comboClose.Items.Count, close); } } private int isSelectedIndexSafe(int itemCount, int value) { /*This method is to make sure that a combo selectedIndex is not set to a number greater than the number of elements which it contains. This arises when someone deletes a column from the Prototype Editor. An exception can arise with some of the series editor combos... Now we will set that combo's index to zero if that situation arises*/ if (value >= itemCount) { return 0; } else { return value; } } private void addSeriesButton_Click(object sender, EventArgs e) { addNewSeries(); List columnHeadings = prototypeEditor1.GetColumnHeadings(); updateSeriesComboBoxes(seriesEditorTabControl.TabPages[seriesEditorTabControl.TabPages.Count - 1], columnHeadings); seriesEditorTabControl.SelectedIndex = seriesEditorTabControl.TabPages.Count - 1; } private void addNewSeries() { string newTabPageString = tabPageTitle("Series", _tabCount); //Prepare the new tab page... TabPage tabPage = new TabPage(newTabPageString); tabPage.BackColor = Color.White; //add the series editor control to the tab page... SeriesEditor s = new SeriesEditor(); s.setLegendTextBox(newTabPageString); tabPage.Controls.Add(s); s.Dock = DockStyle.Fill; s.TabPage = tabPage; s.SeriesEditorName = makeSeriesEditorName(); // Set chart s.WorkingChart = chart1; //Add the tab page. seriesEditorTabControl.TabPages.Add(tabPage); //Add the new series to the chart Series newSeries = new Series(newTabPageString); newSeries.ChartType = SeriesChartType.Line; newSeries.BorderWidth = 4; //reasonable default line width chart1.Series.Add(newSeries); chart1.Series[chart1.Series.Count - 1].EmptyPointStyle.BorderWidth = 0; chart1.Series[chart1.Series.Count - 1].EmptyPointStyle.MarkerStyle = MarkerStyle.None; chart1.Series[chart1.Series.Count - 1]["EmptyPointValue"] = "Zero"; s.TabSeries(ref newSeries); _tabCount++; chart1.ApplyPaletteColors(); //Must call this to fish out the auto-generated color of a freshly added series! Color color = chart1.Series[chart1.Series.Count - 1].Color; s.setcolorSamplePanel(color); //Set tab control s.WorkingTabSheet = seriesEditorTabControl; } private void deleteSeriesButton_Click(object sender, EventArgs e) { //if there's only one tab remaining, do not remove... if (seriesEditorTabControl.TabPages.Count == 1) { return; } int selected = seriesEditorTabControl.SelectedIndex; SeriesEditor sEditor = (SeriesEditor)seriesEditorTabControl.TabPages[selected].Controls[0]; string editorName = (string)sEditor.SeriesEditorName; //for retrieving relevant annotations seriesEditorTabControl.TabPages.RemoveAt(selected); //Remove the series corresponding to that tab index chart1.Series.RemoveAt(selected); //remove annotations(if any) associated with that particular series. clearAnnotations(editorName); //reset color of series foreach (TabPage tab in seriesEditorTabControl.TabPages) { SeriesEditor sEdit = (SeriesEditor)tab.Controls[0]; //There's only one control in tab sheet- it's the SeriesEditor control Series s = sEdit.getSeries(); s.Color = sEdit.getColorSamplePanel(); } } private string makeSeriesEditorName() { /*We shall name each new Series Editor object with randomly generated 4 character name*/ string newName = ""; while (true) { int tally = 0; newName = GetRandomString(4); foreach (TabPage tab in seriesEditorTabControl.TabPages) { //tab page only has *one* control-it's the SeriesEditor SeriesEditor sEdit = (SeriesEditor)tab.Controls[0]; if (sEdit.SeriesEditorName != newName) { tally++; } } if (tally == seriesEditorTabControl.TabPages.Count) { break; } } return newName; } public string GetRandomString(int length) { //code from http://blog.codeeffects.com/Article/Generate-Random-Numbers-And-Strings-C-Sharp string[] array = new string[54] { "0","2","3","4","5","6","8","9", "a","b","c","d","e","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","y","z", "A","B","C","D","E","F","G","H","J","K","L","M","N","P","R","S","T","U","V","W","X","Y","Z" }; System.Text.StringBuilder sb = new System.Text.StringBuilder(); for (int i = 0; i < length; i++) sb.Append(array[GetRandomNumber(53)]); return sb.ToString(); } public int GetRandomNumber(int maxNumber) { //code from http://blog.codeeffects.com/Article/Generate-Random-Numbers-And-Strings-C-Sharp if (maxNumber < 1) throw new System.Exception("The maxNumber value should be greater than 1"); byte[] b = new byte[4]; new System.Security.Cryptography.RNGCryptoServiceProvider().GetBytes(b); int seed = (b[0] & 0x7f) << 24 | b[1] << 16 | b[2] << 8 | b[3]; System.Random r = new System.Random(seed); return r.Next(1, maxNumber); } public void clearAnnotations(string seriesEditorName) { List temp = new List(); foreach (Annotation a in chart1.Annotations) { AnnotationTag t = (AnnotationTag)a.Tag; if (t._name == seriesEditorName) { continue; } else { temp.Add(a); } } chart1.Annotations.Clear(); foreach (Annotation a in temp) { chart1.Annotations.Add(a); } SeriesEditor.checkAnnotationVisible(chart1); } private string tabPageTitle(string word, int pages) { string retVal = word + pages.ToString(); foreach (TabPage tab in seriesEditorTabControl.TabPages) { if (tab.Text.Equals(word + pages.ToString())) { pages++; retVal = word + pages.ToString(); } } return retVal; } private void saveButton_Click(object sender, EventArgs e) { LayoutManager layoutManager = LayoutManager.Instance(); SaveFileDialog dialog = new SaveFileDialog(); if (null != layoutManager.Directory) dialog.InitialDirectory = layoutManager.Directory; dialog.Filter = "TIQ Window files|*.WTI|All files|*.*"; dialog.DefaultExt = "*.WTI"; string fileName = ""; if (_fileNameSaved == null) { fileName = "My TIQ Chart"; } else { fileName = _fileNameSaved; } dialog.FileName = FileNameMethod.QuoteFileName(fileName); if (dialog.ShowDialog() == DialogResult.OK) { layoutManager.SaveOne(this, dialog.FileName); _fileNameSaved = Path.GetFileName(dialog.FileName); } dialog.Dispose(); } private void btnDuplicate_Click(object sender, EventArgs e) { LayoutManager.Instance().Duplicate(this); } //The below methods are tailored specifically when one clicks on a cell of the CsVPreview window... //There's only one tab sheet in the Series Editor public PrototypeEditor getPrototypeEditor() { return prototypeEditor1; } public void setStockSymbol(string symbol) { txtStockSymbol.Text = symbol.Trim(); } public void produceChart(int selectedRowIndex) { selectedRwIndex = selectedRowIndex; //the selectedRowIndex would correspond to the day which the annotation arrow would point to. foreach (Control c in seriesEditorTabControl.TabPages[0].Controls) { if (c as SeriesEditor != null) { SeriesEditor seriesEditor = (SeriesEditor)c; seriesEditor.WorkingChart = chart1; seriesEditor.SeriesEditorName = makeSeriesEditorName(); seriesEditorNameCsvPreview = seriesEditor.SeriesEditorName; seriesEditor.makeSettingsFromCsvCellClick(); requestData(); break; } } } private void addAnnotationToPoint() { ArrowAnnotation annotationArrow = new ArrowAnnotation(); annotationArrow.LineWidth = 1; annotationArrow.ArrowSize = 2; annotationArrow.Height = 8; annotationArrow.Width = 0; annotationArrow.LineColor = Color.Black; annotationArrow.BackColor = Color.Magenta; annotationArrow.ArrowStyle = ArrowStyle.Simple; if (selectedRwIndex < chart1.Series[0].Points.Count) { DataPoint p = chart1.Series[0].Points[selectedRwIndex]; annotationArrow.Tag = new AnnotationTag(seriesEditorNameCsvPreview, p.YValues[0]); annotationArrow.AnchorDataPoint = p; annotationArrow.AnchorY = p.YValues[0]; annotationArrow.AnchorOffsetY = 1.5; chart1.Annotations.Add(annotationArrow); SeriesEditor.checkAnnotationVisible(chart1); } } } }