//******************************************************************************************************
// 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
}
}