/* * XLSX.File.Writer.cs * * Copyright © 2017 * Diversified Technical Systems, Inc. * All Rights Reserved */ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet; using DTS.Common.Enums; using DTS.Common.Enums.Sensors; using DTS.Common.Interface.ExportData; using DTS.Common.SerializationPlus; using DTS.Common.Utilities.Logging; using DTS.Slice.Control; // ReSharper disable PossiblyMistakenUseOfParamsMethod namespace DTS.Serialization.XLSX { public partial class File { /// /// implementation of the Serialization.File.Writer class for XLSX /// http://fogbugz/fogbugz/default.asp?9920 /// public class Writer : Writer, IWriter { #region properties /// /// the owning file that controls this writer /// internal File WriterParent { get; } /// /// controls whether to export ADC or not /// public bool ExportADC { get; set; } /// /// controls whether to export EU or not /// public bool ExportEU { get; set; } /// /// controls whether to export MV or not /// public bool ExportMv { get; set; } /// /// the starting /// public double Start { get; set; } public double Stop { get; set; } public bool Filtered { get; set; } /// /// FB 6410 The list of user selected headers to export /// public List ExportHeaders { get; set; } #endregion #region enums and constants /// /// every 1000 samples update progress /// private const int UPDATE_INTERVAL = 1000; #endregion #region methods /// /// writes out test to given path /// /// /// /// /// /// /// /// public void Write(string pathname, string id, Test test, bool bFiltering, bool includeGroupNameInISOExport, double minStartTime, int dataCollectionLength) { } /// /// returns a datascaler for the given channel /// /// /// public static Common.DAS.Concepts.DataScaler GetDataScaler(Test.Module.AnalogInputChannel currentAnalogChannel) { var scaler = new Common.DAS.Concepts.DataScaler { IsInverted = currentAnalogChannel.IsInverted, IEPE = currentAnalogChannel.Bridge == SensorConstants.BridgeType.IEPE, UnitConversion = currentAnalogChannel.UnitConversion, BasedOnOutputAtCapacity = currentAnalogChannel.AtCapacity, CapacityOutputIsBasedOn = currentAnalogChannel.CapacityOutputIsBasedOn, SensitivityUnits = currentAnalogChannel.SensitivityUnits, Multiplier = currentAnalogChannel.Multiplier, UserOffsetEU = currentAnalogChannel.UserOffsetEU }; scaler.SetLinearizationFormula(currentAnalogChannel.LinearizationFormula); scaler.SetScaleFactorMv(currentAnalogChannel.Data.ScaleFactorMv); scaler.SetScaleFactorEU(currentAnalogChannel.Data.ScaleFactorEU); scaler.SetUseEUScaleFactors(currentAnalogChannel.Data.UseEUScaleFactors); scaler.SetMvPerEu(currentAnalogChannel.Data.MvPerEu); try { scaler.SetInitialOffset(currentAnalogChannel.InitialOffset); scaler.ZeroMethodType = currentAnalogChannel.ZeroMethod; scaler.SetRemovedADC(currentAnalogChannel.RemovedADC); scaler.SetRemovedInternalADC(currentAnalogChannel.RemovedInternalADC); scaler.SetDataZeroLevelADC(currentAnalogChannel.DataZeroLevelAdc); scaler.SetZeroMvInADC(currentAnalogChannel.ZeroMvInADC); try { //note the window average is the average over time when the average is not in your dataset //sliceware is the only software that sets this currently scaler.SetWindowAverageADC(currentAnalogChannel.WindowAverageADC); } catch (System.Exception ex) { APILogger.Log("WindowAverageADC failed to set", ex); } } catch (System.Exception ex) { APILogger.Log("Failed to set parameters on scaler", ex); } scaler.NominalExcitationVoltage = currentAnalogChannel.ExcitationVoltage; if (currentAnalogChannel.MeasuredExcitationVoltageValid) { try { scaler.MeasuredExcitationVoltage = currentAnalogChannel.MeasuredExcitationVoltage; } catch (System.Exception ex) { APILogger.Log("failed to get measured excitation voltage", ex); } } if (currentAnalogChannel.FactoryExcitationVoltageValid) { try { scaler.FactoryExcitationVoltage = currentAnalogChannel.FactoryExcitationVoltage; } catch (System.Exception ex) { APILogger.Log("failed to get factory excitation", ex); } } //14469 Excel export EU values incorrect for digital input channel //digital properties were not being set scaler.Digital = currentAnalogChannel.IsDigital(); scaler.DigitalMode = currentAnalogChannel.DigitalMode; scaler.SetDigitalMultiplier(currentAnalogChannel.DigitalMultiplier); scaler.ProportionalToExcitation = currentAnalogChannel.ProportionalToExcitation; return scaler; } /// /// updates the progress if possible /// /// /// private void UpdateProgress(double dValue, TickEventHandler tickEventHandler) { tickEventHandler?.Invoke(this, dValue); } private static readonly Dictionary _rowIndexToRow = new Dictionary(); /// /// gets a cell given a work sheet and a strong cell reference /// cell reference should be in the form SheetId!$Column$Row, /// like DefinedNames use /// /// /// /// /// /// protected Cell GetCell(Worksheet worksheet, string xColumn, uint rowIndex, bool bLookForCell = true) { var nameref = string.Format("{0}{1}", xColumn, rowIndex); // ReSharper disable once ReplaceWithSingleCallToFirstOrDefault Row theRow; if (_rowIndexToRow.ContainsKey(rowIndex)) { theRow = _rowIndexToRow[rowIndex]; } else { return InsertCellInWorksheet(xColumn, rowIndex, worksheet.WorksheetPart, nameref, false); } if (!bLookForCell) { InsertCellInWorksheet(xColumn, rowIndex, worksheet.WorksheetPart, nameref, false); } // ReSharper disable once ReplaceWithSingleCallToFirstOrDefault var theCell = theRow.Descendants().Where(c => c.CellReference == nameref).FirstOrDefault(); return theCell ?? InsertCellInWorksheet(xColumn, rowIndex, worksheet.WorksheetPart, nameref, false); } // Given a column name, a row index, and a WorksheetPart, inserts a cell into the worksheet. // If the cell already exists, returns it. private static Cell InsertCellInWorksheet(string columnName, uint rowIndex, WorksheetPart worksheetPart, string cellReference, bool bLookForCell) { var worksheet = worksheetPart.Worksheet; var sheetData = worksheet.GetFirstChild(); // If the worksheet does not contain a row with the specified row index, insert one. Row row; if (_rowIndexToRow.ContainsKey(rowIndex)) { row = _rowIndexToRow[rowIndex]; } else { row = new Row { RowIndex = rowIndex }; _rowIndexToRow[rowIndex] = row; sheetData.Append(row); } // If there is not a cell with the specified column name, insert one. if (bLookForCell) { var matches = row.Elements().Where(c => c.CellReference.Value == columnName + rowIndex); //resharper suggests these should be made into an array first var matchesAsArray = matches as Cell[] ?? matches.ToArray(); if (matchesAsArray.Any()) { return matchesAsArray.First(); } } // Cells must be in sequential order according to CellReference. Determine where to insert the new cell. var refCell = row.Elements().Where(cell => cell.CellReference.Value.Length == cellReference.Length).FirstOrDefault(cell => string.Compare(cell.CellReference.Value, cellReference, StringComparison.OrdinalIgnoreCase) > 0); var newCell = new Cell { CellReference = cellReference }; row.InsertBefore(newCell, refCell); return newCell; } protected WorksheetPart GetWorksheetPartByName(SpreadsheetDocument document, string sheetName) { var sheets = document.WorkbookPart.Workbook.GetFirstChild(). Elements().Where(s => s.Name == sheetName); var enumerable = sheets as Sheet[] ?? sheets.ToArray(); if (!enumerable.Any()) { // The specified worksheet does not exist. return null; } var relationshipId = enumerable.First().Id.Value; var worksheetPart = (WorksheetPart)document.WorkbookPart.GetPartById(relationshipId); return worksheetPart; } private Dictionary _stringLookup; protected int InsertSharedStringItem(string text, SharedStringTablePart shareStringPart) { if (null == text) { text = ""; } // If the part does not contain a SharedStringTable, create one. if (shareStringPart.SharedStringTable == null) { shareStringPart.SharedStringTable = new SharedStringTable(); } int i; if (null == _stringLookup) { i = 0; _stringLookup = new Dictionary(); foreach (var item in shareStringPart.SharedStringTable.Elements()) { _stringLookup[item.InnerText] = i++; } } //if the string is already in the table, return it's index, otherwise add a new string to the table and return the new index if (_stringLookup.ContainsKey(text)) { return _stringLookup[text]; } i = _stringLookup.Count; shareStringPart.SharedStringTable.AppendChild(new SharedStringItem(new Text(text))); shareStringPart.SharedStringTable.Save(); _stringLookup[text] = i; return i; } private void WriteTime(WorksheetPart ws, string column, uint row, DateTime time, SharedStringTablePart sharedString) { var cell = GetCell(ws.Worksheet, column, row); cell.DataType = new EnumValue(CellValues.SharedString); cell.CellValue = new CellValue(InsertSharedStringItem(time.ToShortTimeString(), sharedString).ToString()); cell.StyleIndex = _dateFormatIndex; } /// /// FB 6410 The override for WriteTime to use the correct row for the requested headerName /// /// /// /// /// /// private void WriteTime(WorksheetPart ws, string column, string headerName, DateTime time, SharedStringTablePart sharedString) { if (!_headerRowLineIndex.ContainsKey(headerName)) { return; } WriteTime(ws, column, _headerRowLineIndex[headerName], time, sharedString); } private void WriteDate(WorksheetPart ws, string column, uint row, DateTime date) { var cell = GetCell(ws.Worksheet, column, row); cell.DataType = new EnumValue(CellValues.Number); cell.CellValue = new CellValue(date.ToOADate().ToString(CultureInfo.InvariantCulture)); cell.StyleIndex = _dateFormatIndex; } /// /// FB 6410 The override for WriteDate to use the correct row for the requested headerName /// /// /// /// /// private void WriteDate(WorksheetPart ws, string column, string headerName, DateTime date) { if (!_headerRowLineIndex.ContainsKey(headerName)) { return; } WriteDate(ws, column, _headerRowLineIndex[headerName], date); } private void WriteDouble(WorksheetPart ws, string column, uint row, double value) { var cell = GetCell(ws.Worksheet, column, row, false); cell.CellValue = new CellValue(value.ToString(CultureInfo.InvariantCulture)); } /// /// FB 6410 The override for WriteDouble to use the correct row for the requested headerName /// /// /// /// /// private void WriteDouble(WorksheetPart ws, string column, string headerName, double value) { if (!_headerRowLineIndex.ContainsKey(headerName)) { return; } WriteDouble(ws, column, _headerRowLineIndex[headerName], value); } private void WriteString(WorksheetPart ws, string column, uint row, SharedStringTablePart sharedStringTablePart, string value) { var cell = GetCell(ws.Worksheet, column, row, false); cell.CellValue = new CellValue(InsertSharedStringItem(value, sharedStringTablePart).ToString()); cell.DataType = new EnumValue { Value = CellValues.SharedString }; } /// /// FB 6410 The override for WriteString to use the correct row for the requested headerName /// /// /// /// /// /// private void WriteString(WorksheetPart ws, string column, string headerName, SharedStringTablePart sharedStringTablePart, string value) { //If we can't find the headerName that header is not selected by user and it's not in this dictionary skip this header if (!_headerRowLineIndex.ContainsKey(headerName)) { return; } // Do not eneter the header name twice if it's already entered if (_alreadyEnteredHeader.Contains(value)) { return; } WriteString(ws, column, _headerRowLineIndex[headerName], sharedStringTablePart, value); _alreadyEnteredHeader.Add(headerName); } private List _alreadyEnteredHeader = new List(); private static string GetColumn(int index) { var dividend = index + 2;//we are starting at b... var columnName = string.Empty; while (dividend > 0) { var modulo = (dividend - 1) % 26; columnName = Convert.ToChar(65 + modulo) + columnName; dividend = (dividend - modulo) / 26; } return columnName; } private uint _dateFormatIndex; private void AddStyleSheet(SpreadsheetDocument sp) { var stylesheet = sp.WorkbookPart.WorkbookStylesPart.Stylesheet; stylesheet.CellFormats.AppendChild(new CellFormat { NumberFormatId = 14, ApplyNumberFormat = true }); _dateFormatIndex = stylesheet.CellFormats.Count; stylesheet.Save(); } /// /// Insert an empty row after the last entered row /// /// private void InsertRow(WorksheetPart worksheetPart) { SheetData sheetData = worksheetPart.Worksheet.GetFirstChild(); Row lastRow = sheetData.Elements().LastOrDefault(); if (lastRow != null) { sheetData.InsertAfter(new Row() { RowIndex = (lastRow.RowIndex + 1) }, lastRow); } } private Dictionary _headerRowLineIndex = new Dictionary(); /// /// This dictionary holds the datascaler for the AICs for the test. This was done for performance reasons. There /// are several for/foreach loops that are in the write function that all need the scaler, but dont all need to /// get it everytime. /// private Dictionary _aicToScaler; /// /// writes out test to given path /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// public void Write(string pathname, string id, string dataFolder, Test test, bool bFiltering, bool includeGroupNameInISOExport, FilteredData fd, Test.Module.Channel tmChannel, int channelNumber, BeginEventHandler beginEventHandler, CancelEventHandler cancelEventHandler, EndEventHandler endEventHandler, TickEventHandler tickEventHandler, ErrorEventHandler errorEventHandler, CancelRequested cancelRequested, double minStartTime, int dataCollectionLength) { try { _rowIndexToRow.Clear(); _headerRowLineIndex.Clear(); _aicToScaler = new Dictionary(); _stringLookup = null; beginEventHandler?.Invoke(this, 100); //pathname has the full path of our destination file var sourceFile = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ReportTemplates", "XLSXExportTemplate.xlsx"); var expectedWrites = double.NaN; if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(pathname))) { System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(pathname)); } System.IO.File.Copy(sourceFile, pathname, true); //FB 6410 Build the dictionary to keep the name of the headers need to be exported along with the actual row number in the excel file uint headerRowIndex = 1; _headerRowLineIndex.Add(XLSXExportHeaderLine.Headers.GetDescription(), headerRowIndex); foreach (var header in ExportHeaders) { if (header.IsSelected) { headerRowIndex++; _headerRowLineIndex.Add(header.HeaderName, headerRowIndex); } } headerRowIndex++; _headerRowLineIndex.Add(XLSXExportHeaderLine.DataStart.GetDescription(), headerRowIndex); headerRowIndex++; _headerRowLineIndex.Add(XLSXExportHeaderLine.Labels.GetDescription(), headerRowIndex); var maxSampleRate = test.Channels.Select(ch => ch.ParentModule.SampleRateHz).Max(); //14659 Excel (xlsx) export all (filtered) fails, but says export finished in green //we need to use channel order rather than display order as display order in some data sets //so that we can index the channels var channels = new List(); channels.AddRange(test.Channels); channels.Sort((a, b) => { var res = a.AbsoluteDisplayOrder.CompareTo(b.AbsoluteDisplayOrder); if (res != 0) { return res; } return a.Number.CompareTo(b.Number); }); using (var sp = SpreadsheetDocument.Open(pathname, true)) { AddStyleSheet(sp); var shareStringPart = sp.WorkbookPart.GetPartsOfType().First(); sp.WorkbookPart.Workbook.CalculationProperties.ForceFullCalculation = true; sp.WorkbookPart.Workbook.CalculationProperties.FullCalculationOnLoad = true; var dataWs = GetWorksheetPartByName(sp, "Data"); var rows = dataWs.Worksheet.Descendants(); //FB 6410 insert rows for each selected header in the excel sheet which will be filled later on for (int i = 0; i < _headerRowLineIndex.Count - 1; i++) { InsertRow(dataWs); } foreach (var row in rows) { _rowIndexToRow[row.RowIndex] = row; } WriteString(dataWs, "A", XLSXExportHeaderLine.TestDate.GetDescription(), shareStringPart, XLSXExportHeaderLine.TestDate.GetDescription()); WriteDate(dataWs, "B", XLSXExportHeaderLine.TestDate.GetDescription(), test.InceptionDate); WriteString(dataWs, "A", XLSXExportHeaderLine.TestTime.GetDescription(), shareStringPart, XLSXExportHeaderLine.TestTime.GetDescription()); WriteTime(dataWs, "B", XLSXExportHeaderLine.TestTime.GetDescription(), test.InceptionDate, shareStringPart); WriteString(dataWs, "A", XLSXExportHeaderLine.TestId.GetDescription(), shareStringPart, XLSXExportHeaderLine.TestId.GetDescription()); WriteString(dataWs, "B", XLSXExportHeaderLine.TestId.GetDescription(), shareStringPart, id); WriteString(dataWs, "A", XLSXExportHeaderLine.TestDescription.GetDescription(), shareStringPart, XLSXExportHeaderLine.TestDescription.GetDescription()); WriteString(dataWs, "B", XLSXExportHeaderLine.TestDescription.GetDescription(), shareStringPart, test.Description); //go through all the channels, and include whatever data is requested foreach (var channel in channels) { //for now ignore the channel if it's not an analoginputchannel, this probably includes everything except maybe the calculated channels ... if (!(channel is Test.Module.AnalogInputChannel aic)) { continue; } //this should divide evenly, so ceiling is necessary in theory var rate = Convert.ToInt32(Math.Ceiling(maxSampleRate / aic.ParentModule.SampleRateHz)); var iChannel = channels.IndexOf(channel); if (double.IsNaN(expectedWrites)) { expectedWrites = (double)test.Channels.Count * dataCollectionLength; } var column = GetColumn(iChannel); WriteString(dataWs, "A", XLSXExportHeaderLine.SampleRate.GetDescription(), shareStringPart, XLSXExportHeaderLine.SampleRate.GetDescription()); WriteDouble(dataWs, column, XLSXExportHeaderLine.SampleRate.GetDescription(), maxSampleRate); WriteString(dataWs, "A", XLSXExportHeaderLine.HardwareAntiAliasFilter.GetDescription(), shareStringPart, XLSXExportHeaderLine.HardwareAntiAliasFilter.GetDescription()); WriteDouble(dataWs, column, XLSXExportHeaderLine.HardwareAntiAliasFilter.GetDescription(), aic.ParentModule.AaFilterRateHz); WriteString(dataWs, "A", XLSXExportHeaderLine.DataChannelNumber.GetDescription(), shareStringPart, XLSXExportHeaderLine.DataChannelNumber.GetDescription()); WriteDouble(dataWs, column, XLSXExportHeaderLine.DataChannelNumber.GetDescription(), (double)1 + iChannel); var isoCode = ""; if (null != WriterParent) { switch (WriterParent.ISOViewMode) { case Common.Enums.IsoViewMode.ISOOnly: isoCode = aic.IsoCode; break; case Common.Enums.IsoViewMode.ISOAndUserCode: isoCode = $"{aic.IsoCode}/{aic.UserCode}"; break; case Common.Enums.IsoViewMode.UserCodeOnly: isoCode = aic.UserCode; break; case Common.Enums.IsoViewMode.ChannelNameOnly: isoCode = string.Empty; break; } } WriteString(dataWs, "A", XLSXExportHeaderLine.IsoCode.GetDescription(), shareStringPart, XLSXExportHeaderLine.IsoCode.GetDescription()); WriteString(dataWs, column, XLSXExportHeaderLine.IsoCode.GetDescription(), shareStringPart, isoCode); WriteString(dataWs, "A", XLSXExportHeaderLine.ChannelDescription.GetDescription(), shareStringPart, XLSXExportHeaderLine.ChannelDescription.GetDescription()); WriteString(dataWs, column, XLSXExportHeaderLine.ChannelDescription.GetDescription(), shareStringPart, aic.ChannelName2); WriteString(dataWs, "A", XLSXExportHeaderLine.ChannelLocation.GetDescription(), shareStringPart, XLSXExportHeaderLine.ChannelLocation.GetDescription()); WriteString(dataWs, "A", XLSXExportHeaderLine.SensorSerialNumber.GetDescription(), shareStringPart, XLSXExportHeaderLine.SensorSerialNumber.GetDescription()); WriteString(dataWs, column, XLSXExportHeaderLine.SensorSerialNumber.GetDescription(), shareStringPart, aic.SerialNumber); var filter = SaeJ211Filter.Parse(aic.SoftwareFilter); WriteString(dataWs, "A", XLSXExportHeaderLine.SoftwareFilter.GetDescription(), shareStringPart, XLSXExportHeaderLine.SoftwareFilter.GetDescription()); //FB 18024 Show NONE for unfiltered if (Filtered) { WriteString(dataWs, column, XLSXExportHeaderLine.SoftwareFilter.GetDescription(), shareStringPart, filter.Name); } else { WriteString(dataWs, column, XLSXExportHeaderLine.SoftwareFilter.GetDescription(), shareStringPart, "NONE"); } if (bFiltering) { WriteString(dataWs, "A", XLSXExportHeaderLine.SoftwareFilterDb.GetDescription(), shareStringPart, XLSXExportHeaderLine.SoftwareFilterDb.GetDescription()); //FB 18024 Show NONE for unfiltered if (Filtered) { WriteDouble(dataWs, column, XLSXExportHeaderLine.SoftwareFilterDb.GetDescription(), bFiltering ? filter.CutoffFrequencyHz : 0D); } else { WriteString(dataWs, column, XLSXExportHeaderLine.SoftwareFilterDb.GetDescription(), shareStringPart, "NONE"); } } WriteString(dataWs, "A", XLSXExportHeaderLine.EngineeringUnits.GetDescription(), shareStringPart, XLSXExportHeaderLine.EngineeringUnits.GetDescription()); WriteString(dataWs, column, XLSXExportHeaderLine.EngineeringUnits.GetDescription(), shareStringPart, aic.EngineeringUnits); var preTriggerSamples = aic.ParentModule.TriggerSampleNumbers[0] - (double)aic.ParentModule.StartRecordSampleNumber; if (preTriggerSamples < 0) { preTriggerSamples = 0D; } if (preTriggerSamples > aic.ParentModule.NumberOfSamples) { preTriggerSamples = aic.ParentModule.NumberOfSamples; } if (preTriggerSamples > Start * aic.ParentModule.SampleRateHz) { preTriggerSamples = Math.Truncate(Start * aic.ParentModule.SampleRateHz); } WriteString(dataWs, "A", XLSXExportHeaderLine.UserComment.GetDescription(), shareStringPart, XLSXExportHeaderLine.UserComment.GetDescription()); WriteString(dataWs, "A", XLSXExportHeaderLine.PreZero.GetDescription(), shareStringPart, XLSXExportHeaderLine.PreZero.GetDescription()); WriteDouble(dataWs, column, XLSXExportHeaderLine.PreZero.GetDescription(), rate * preTriggerSamples); var postTriggerSamples = aic.ParentModule.NumberOfSamples - preTriggerSamples; if (postTriggerSamples < 0) { postTriggerSamples = 0D; } if (postTriggerSamples > Stop * aic.ParentModule.SampleRateHz) { postTriggerSamples = Math.Truncate(Stop * aic.ParentModule.SampleRateHz); } WriteString(dataWs, "A", XLSXExportHeaderLine.PostZero.GetDescription(), shareStringPart, XLSXExportHeaderLine.PostZero.GetDescription()); WriteDouble(dataWs, column, XLSXExportHeaderLine.PostZero.GetDescription(), rate * postTriggerSamples); WriteString(dataWs, "A", XLSXExportHeaderLine.DataZero.GetDescription(), shareStringPart, XLSXExportHeaderLine.DataZero.GetDescription()); WriteDouble(dataWs, column, XLSXExportHeaderLine.DataZero.GetDescription(), aic.DataZeroLevelAdc); _aicToScaler[iChannel] = GetDataScaler(aic); WriteString(dataWs, "A", XLSXExportHeaderLine.ScaleEu.GetDescription(), shareStringPart, XLSXExportHeaderLine.ScaleEu.GetDescription()); WriteDouble(dataWs, column, XLSXExportHeaderLine.ScaleEu.GetDescription(), _aicToScaler[iChannel].GetAdcToEuScalingFactor()); WriteString(dataWs, "A", XLSXExportHeaderLine.ScaleMv.GetDescription(), shareStringPart, XLSXExportHeaderLine.ScaleMv.GetDescription()); WriteDouble(dataWs, column, XLSXExportHeaderLine.ScaleMv.GetDescription(), _aicToScaler[iChannel].GetAdcToMvScalingFactor()); WriteString(dataWs, "A", XLSXExportHeaderLine.DataStart.GetDescription(), shareStringPart, XLSXExportHeaderLine.DataStart.GetDescription()); WriteString(dataWs, "A", XLSXExportHeaderLine.Labels.GetDescription(), shareStringPart, XLSXExportHeaderLine.Labels.GetDescription()); WriteString(dataWs, column, XLSXExportHeaderLine.Labels.GetDescription(), shareStringPart, $"Chan {1 + iChannel}: {aic.ChannelName2}"); } dataWs.Worksheet.Save(); } //now that we are done with all the strings, open up as SAX and write all the values //the numeric cells are easy, but non numeric have data types and sharedstringpart s to manipulate, so //that's why I split the two up using (var myDoc = SpreadsheetDocument.Open(pathname, true)) { var workbookPart = myDoc.WorkbookPart; var worksheetPart = GetWorksheetPartByName(myDoc, "Data"); var origninalSheetId = workbookPart.GetIdOfPart(worksheetPart); var replacementPart = workbookPart.AddNewPart(); var replacementPartId = workbookPart.GetIdOfPart(replacementPart); var reader = OpenXmlReader.Create(worksheetPart); var writer = OpenXmlWriter.Create(replacementPart); var filteredData = new double[test.Channels.Count][]; if (Filtered) { var data = filteredData; Parallel.ForEach(channels, channel => { var aic = (Test.Module.AnalogInputChannel)channel; if (null == aic) { return; } var iChannel = channels.IndexOf(channel); var sampleCount = aic.PersistentChannelInfo.Data.Length; data[iChannel] = new double[sampleCount]; for (var iSampleIdx = 0; iSampleIdx < sampleCount; iSampleIdx++) { data[iChannel][iSampleIdx] = _aicToScaler[iChannel].GetEU(aic.PersistentChannelInfo.Data[iSampleIdx]); } var filter = SaeJ211Filter.Parse(aic.SoftwareFilter); data[iChannel] = filter.Apply(data[iChannel], aic.ParentModule.SampleRateHz, UseLegacyTDCSoftwareFiltering); }); filteredData = data; } headerRowIndex++; while (reader.Read()) { if (reader.ElementType == typeof(SheetData) && !reader.IsEndElement) { writer.WriteStartElement(new SheetData()); } else if (reader.ElementType == typeof(SheetData) && reader.IsEndElement) { for (var sampleIndex = 0UL; (int)sampleIndex <= dataCollectionLength; sampleIndex++) { var row = new Row { RowIndex = Convert.ToUInt32(headerRowIndex + sampleIndex) }; writer.WriteStartElement(row); foreach (var channel in channels) { if (!(channel is Test.Module.AnalogInputChannel aic)) { continue; } var iChannel = channels.IndexOf(channel); var dStartTime = (double)aic.ParentModule.StartRecordSampleNumber / aic.ParentModule.SampleRateHz; if (aic.ParentModule.TriggerSampleNumbers.Count > 0) { dStartTime -= (double)aic.ParentModule.TriggerSampleNumbers[0] / aic.ParentModule.SampleRateHz; } //14513 double rounding error causing data to be offset by one sample incorrectly // note that using decimals will fix the imprecision issue, but will use more time ... var delta = Convert.ToDecimal(dStartTime) - Convert.ToDecimal(minStartTime); var channelOffsetStart = Convert.ToInt32(delta * Convert.ToDecimal(aic.ParentModule.SampleRateHz)); var rate = maxSampleRate / aic.ParentModule.SampleRateHz; var indexAtCurrentTime = ((double)sampleIndex - channelOffsetStart) / rate; var thisChannelsIndexAtCurrentTime = Convert.ToInt32(Math.Floor(indexAtCurrentTime)); var step = Convert.ToInt32(Math.Ceiling(indexAtCurrentTime) - thisChannelsIndexAtCurrentTime); if (0 == iChannel) { var time = minStartTime + sampleIndex / maxSampleRate; var c = new Cell { CellValue = new CellValue(time.ToString(CultureInfo.InvariantCulture)) }; writer.WriteElement(c); } var value = Filtered ? filteredData[iChannel][thisChannelsIndexAtCurrentTime] : _aicToScaler[iChannel].GetEU(aic.PersistentChannelInfo.Data[thisChannelsIndexAtCurrentTime]); if (step > 0) //INTERPOLATE { var increment = 0D; if (Filtered) { if ((1 + thisChannelsIndexAtCurrentTime) < filteredData[iChannel].Length) { increment = (filteredData[iChannel][1 + thisChannelsIndexAtCurrentTime] - value) / rate; } else { increment = (value - filteredData[iChannel][thisChannelsIndexAtCurrentTime - 1]) / rate; } } else { if ((1 + thisChannelsIndexAtCurrentTime) < aic.PersistentChannelInfo.Data.Length) { increment = (_aicToScaler[iChannel].GetEU(aic.PersistentChannelInfo.Data[thisChannelsIndexAtCurrentTime + 1]) - value) / rate; } else { increment = (value - _aicToScaler[iChannel].GetEU(aic.PersistentChannelInfo.Data[thisChannelsIndexAtCurrentTime - 1])) / rate; } } value = value + increment * step; } var cell = new Cell { CellValue = new CellValue( value.ToString(CultureInfo.InvariantCulture)) }; writer.WriteElement(cell); } if (0 == sampleIndex % UPDATE_INTERVAL) { var currentWrite = (double)sampleIndex * test.Channels.Count; UpdateProgress(100D * currentWrite / expectedWrites, tickEventHandler); } writer.WriteEndElement(); } writer.WriteEndElement(); } else { if (reader.ElementType == typeof(Row) && reader.IsStartElement) { writer.WriteElement(reader.LoadCurrentElement()); } else { if (reader.IsStartElement) { writer.WriteStartElement(reader); } else if (reader.IsEndElement) { writer.WriteEndElement(); } } } } reader.Close(); writer.Close(); var sheet = workbookPart.Workbook.Descendants().Where(s => s.Id.Value.Equals(origninalSheetId)).First(); sheet.Id.Value = replacementPartId; workbookPart.DeletePart(worksheetPart); filteredData = null; } foreach (var channel in test.Channels) { if (!(channel is Test.Module.AnalogInputChannel aic)) continue; aic.PersistentChannelInfo.UnSet(); // These garbage collection calls cause a significant performance hit and were removed becasue of this. They // were probably put here when DP was still 32bit only. //System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.Batch; //System.Runtime.GCSettings.LargeObjectHeapCompactionMode = System.Runtime.GCLargeObjectHeapCompactionMode.CompactOnce; //GC.Collect(); } } catch (System.Exception ex) { APILogger.Log("encountered problem writing XLSX test files", ex); errorEventHandler?.Invoke(this, ex); } finally { tickEventHandler?.Invoke(this, 100D); endEventHandler?.Invoke(this); } } #endregion /// /// constructs the writer with a given file and encoding /// /// /// internal Writer(File fileType, int encoding) : base(fileType, encoding) { ExportADC = false; ExportEU = true; ExportMv = false; WriterParent = fileType; } /// /// initializes the writer /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// public void Initialize(string pathname, string id, string dataFolder, Test test, bool bFiltering, bool includeGroupNameInISOExport, FilteredData fd, Test.Module.Channel tmChannel, int channelNumber, BeginEventHandler beginEventHandler, CancelEventHandler cancelEventHandler, EndEventHandler endEventHandler, TickEventHandler tickEventHandler, ErrorEventHandler errorEventHandler, CancelRequested cancelRequested) { } } } }