//****************************************************************************************************** // PQDSFile.cs - Gbtc // // Copyright © 2020, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/eclipse-1.0.php // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 03/06/2020 - Christoph Lackner // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; namespace GSF.PQDS { /// /// Class that represents a PQDS file. /// public class PQDSFile { #region[Properties] private List m_metaData; private List m_Data; private DateTime m_initialTS; #endregion[Properties] #region[Constructors] /// /// Creates a new PQDS file. /// /// Measurment data to be included as /// Timestamp used as the beginning of the PQDS file /// List of MetaData to be included in the PQDS file as public PQDSFile(List metaData, List dataSeries, DateTime initialTimeStamp) { if (metaData is null) { this.m_metaData = new List(); } else { this.m_metaData = metaData; } this.m_initialTS = initialTimeStamp; this.m_Data = dataSeries; } /// /// Creates a new PQDS file. /// public PQDSFile() { this.m_metaData = new List(); this.m_Data = new List(); } #endregion[Constructors] #region[Methods] private void GetStartTime() { DateTime result; int? day = null; int? month = null; int? year = null; if (this.m_metaData.Select(item => item.Key).Contains("eventdate")) { string val = ((MetaDataTag)this.m_metaData.Find(item => item.Key == "eventdate")).Value; if (DateTime.TryParseExact(val, "MM/dd/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { day = result.Day; month = result.Month; year = result.Year; } } if (day is null) { if (this.m_metaData.Select(item => item.Key).Contains("eventday")) { day = ((MetaDataTag)this.m_metaData.Find(item => item.Key == "eventday")).Value; } else { day = DateTime.Now.Day; } } if (month is null) { if (this.m_metaData.Select(item => item.Key).Contains("eventmonth")) { month = ((MetaDataTag)this.m_metaData.Find(item => item.Key == "eventmonth")).Value; } else { month = DateTime.Now.Month; } } if (year is null) { if (this.m_metaData.Select(item => item.Key).Contains("eventyear")) { year = ((MetaDataTag)this.m_metaData.Find(item => item.Key == "eventyear")).Value; } else { year = DateTime.Now.Year; } } int? hour = null; int? minute = null; int? second = null; if (this.m_metaData.Select(item => item.Key).Contains("eventtime")) { string val = ((MetaDataTag)this.m_metaData.Find(item => item.Key == "eventtime")).Value; if (DateTime.TryParseExact(val, "HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { hour = result.Hour; minute = result.Minute; second = result.Second; } } if (hour is null) { if (this.m_metaData.Select(item => item.Key).Contains("eventhour")) { hour = ((MetaDataTag)this.m_metaData.Find(item => item.Key == "eventhour")).Value; } else { hour = DateTime.Now.Hour; } } if (minute is null) { if (this.m_metaData.Select(item => item.Key).Contains("eventminute")) { minute = ((MetaDataTag)this.m_metaData.Find(item => item.Key == "eventminute")).Value; } else { minute = DateTime.Now.Minute; } } if (second is null) { if (this.m_metaData.Select(item => item.Key).Contains("eventsecond")) { second = ((MetaDataTag)this.m_metaData.Find(item => item.Key == "eventsecond")).Value; } else { second = DateTime.Now.Second; } } result = new DateTime((int)year, (int)month, (int)day, (int)hour, (int)minute, (int)second); this.m_initialTS = result; } private MetaDataTag CreateMetaData(string[] flds) { string dataTypeString = flds[3].Trim().ToUpper(); PQDSMetaDataType dataType; switch (dataTypeString) { case "N": { dataType = PQDSMetaDataType.Numeric; break; } case "E": { dataType = PQDSMetaDataType.Enumeration; break; } case "B": { dataType = PQDSMetaDataType.Binary; break; } case "A": { dataType = PQDSMetaDataType.AlphaNumeric; break; } default: { dataType = PQDSMetaDataType.Text; break; } } string key = flds[0].Trim().ToLower(); string note = flds[4].Trim('"'); string unit = flds[2].Trim('"'); switch (dataType) { case (PQDSMetaDataType.AlphaNumeric): { string value = flds[1].Trim('"'); return new MetaDataTag(key, value, dataType, unit, note); } case (PQDSMetaDataType.Text): { string value = flds[1].Trim('"'); return new MetaDataTag(key, value, dataType, unit, note); } case (PQDSMetaDataType.Enumeration): { int value = Convert.ToInt32(flds[1].Trim('"')); return new MetaDataTag(key, value, dataType, unit, note); } case (PQDSMetaDataType.Numeric): { double value = Convert.ToDouble(flds[1].Trim('"')); return new MetaDataTag(key, value, dataType, unit, note); } case (PQDSMetaDataType.Binary): { Boolean value = Convert.ToBoolean(flds[1].Trim('"')); return new MetaDataTag(key, value, dataType, unit, note); } default: { string value = flds[1].Trim('"'); return new MetaDataTag(key, value, dataType, unit, note); } } } private Boolean IsDataHeader(string line) { if (!line.Contains(",")) return false; String[] flds = line.Split(','); if (flds[0].ToLower().Trim() == "waveform-data") return true; return false; } /// /// List of included Metadata tags. /// public List MetaData { get { return this.m_metaData; } } /// /// List of data included in PQDS file as . /// public List Data { get { return this.m_Data; } } /// /// Writes the content to a .csv file. /// /// The to write the data to. /// Progress Token public void WriteToStream(StreamWriter stream, IProgress progress) { int n_data = this.Data.Select((item) => item.Length).Max(); int n_total = n_data + n_data + this.m_metaData.Count() + 1; //create the metadata header List lines = new List(); lines = this.m_metaData.Select(item => item.Write()).ToList(); lines.AddRange(DataLines(n_total, progress)); for (int i = 0; i < lines.Count(); i++) { stream.WriteLine(lines[i]); progress.Report((double)(n_data + i) / n_total); } } /// /// Writes the content to a .csv file. /// /// file name /// Progress Token public void WriteToFile(string file, IProgress progress) { // Open the file and write in each line using (StreamWriter fileWriter = new StreamWriter(File.OpenWrite(file))) { WriteToStream(fileWriter, progress); } } /// /// Writes the content to a .csv file. /// /// file name public void WriteToFile(string file) { Progress prog = new Progress(); WriteToFile(file, prog); } /// /// Writes the content to an output Stream. /// /// The to write the data to. public void WriteToStream(StreamWriter stream) { Progress prog = new Progress(); WriteToStream(stream, prog); } /// /// Reads the content from a PQDS File. /// /// file name public void ReadFromFile(string filename) { Progress prog = new Progress(); ReadFromFile(filename, prog); } /// /// Reads the content from a PQDS File. /// /// file name /// Progress Token public void ReadFromFile(string filename, IProgress progress) { List lines = new List(); // Open the file and read each line using (StreamReader fileReader = new StreamReader(File.OpenRead(filename))) { while (!fileReader.EndOfStream) { lines.Add(fileReader.ReadLine().Trim()); } } int index = 0; String[] flds; // Parse MetaData Section this.m_metaData = new List(); while (!(IsDataHeader(lines[index]))) { if (!lines[index].Contains(",")) { index++; continue; } flds = lines[index].Split(','); if (flds.Count() < 5) { index++; continue; } this.m_metaData.Add(CreateMetaData(flds)); index++; if (index == lines.Count()) { throw new InvalidDataException("PQDS File not valid"); } progress.Report((double)index / (double)lines.Count()); } //Parse Data Header flds = lines[index].Split(','); if (flds.Count() < 2) { throw new InvalidDataException("PQDS File has invalid data section or no data"); } this.m_Data = new List(); List signals = new List(); List> data = new List>(); for (int i = 1; i < flds.Count(); i++) { if (signals.Contains(flds[i].Trim().ToLower())) { continue; } this.m_Data.Add(new DataSeries(flds[i].Trim().ToLower())); signals.Add(flds[i].Trim().ToLower()); data.Add(new List()); } index++; //Parse Data GetStartTime(); while (index < lines.Count()) { if (!lines[index].Contains(",")) { index++; continue; } flds = lines[index].Split(','); if (flds.Count() != (this.m_Data.Count() + 1)) { index++; continue; } DateTime TS; double milliseconds; try { milliseconds = Convert.ToDouble(flds[0].Trim()); double ticks = milliseconds * 10000; TS = this.m_initialTS.AddTicks((int)ticks); } catch { index++; continue; } for (int i = 0; i < signals.Count(); i++) { try { double value = Convert.ToDouble(flds[i + 1].Trim()); data[i].Add(new DataPoint() { Time = TS, Value = value, Milliseconds = milliseconds }); } catch { continue; } } progress.Report((double)index / (double)lines.Count()); index++; } for (int i = 0; i < signals.Count(); i++) { int j = this.m_Data.FindIndex(item => item.Label == signals[i]); this.m_Data[j].Series = data[j]; } } private List DataLines(int n_total, IProgress progress) { List result = new List(); //ensure they all start at the same Time List measurements = this.m_Data.Select(item => item.Label).ToList(); DateTime initalStart = this.m_Data.Select(item => item.Series[0].Time).Min(); List startTime = this.m_Data.Select(item => item.Series[0].Time - initalStart).ToList(); //1 ms difference is ok if (startTime.Max().TotalMilliseconds > 1) { throw new Exception("The measurements start at different times"); } //write the header result.Add("waveform-data," + String.Join(",", measurements)); //write the Data // Logic for skipping datapoints if they don't have the same sampling rate List samplingRates = m_Data.Select(item => item.Length).Distinct().ToList(); int n_data = samplingRates.Max(); Dictionary> reSampling = new Dictionary>(); if (samplingRates.Any(f => ((double)n_data / (double)f) % 1 != 0)) throw new Exception("Sampling Rates in this File do not match and are not multiples of each other."); reSampling = samplingRates.Select(item => new KeyValuePair>(item, (int index, DataSeries ds) => { int n = n_data / item; if (index % n == 0) return ds.Series[index / n].Value; else return double.NaN; })) .ToDictionary(item => item.Key, item => item.Value); for (int i = 0; i < n_data; i++) { TimeSpan dT = m_Data[0].Series[i].Time - m_initialTS; result.Add(Convert.ToString(dT.TotalMilliseconds) + "," + String.Join(",", m_Data.Select(item => { double v = reSampling[item.Length](i, item); if (double.IsNaN(v)) return "NaN".PadLeft(12); return String.Format("{0:F12}", v); }).ToList())); progress.Report((double)i / (double)n_total); } return result; } #endregion[Methods] #region [ Static ] /// /// Reads PQDS file from file path and returns object. /// /// The path to the file. /// New using file contents public static PQDSFile Read(string filePath) { PQDSFile file = new PQDSFile(); file.ReadFromFile(filePath); return file; } #endregion } }