diff --git a/.gitignore b/.gitignore index 03900605..9416d3cc 100644 --- a/.gitignore +++ b/.gitignore @@ -233,4 +233,4 @@ readme.txt **/src/OpenSEE/NUglify.dll **/src/OpenSEE/web.config.backup -**/src/OpenSee/scripts/*.js \ No newline at end of file +**/src/OpenSee/wwwroot/scripts/*.js \ No newline at end of file diff --git a/src/Libraries/FaultAlgorithms/Conductor.cs b/src/Libraries/FaultAlgorithms/Conductor.cs new file mode 100644 index 00000000..5b5a24fe --- /dev/null +++ b/src/Libraries/FaultAlgorithms/Conductor.cs @@ -0,0 +1,122 @@ +//********************************************************************************************************************* +// Conductor.cs +// Version 1.1 and subsequent releases +// +// Copyright © 2013, 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. +// +// -------------------------------------------------------------------------------------------------------------------- +// +// Version 1.0 +// +// Copyright 2012 ELECTRIC POWER RESEARCH INSTITUTE, INC. All rights reserved. +// +// openFLE ("this software") is licensed under BSD 3-Clause license. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// • Redistributions of source code must retain the above copyright notice, this list of conditions and +// the following disclaimer. +// +// • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and +// the following disclaimer in the documentation and/or other materials provided with the distribution. +// +// • Neither the name of the Electric Power Research Institute, Inc. (“EPRI”) nor the names of its contributors +// may be used to endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL EPRI BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// +// This software incorporates work covered by the following copyright and permission notice: +// +// • TVA Code Library 4.0.4.3 - Tennessee Valley Authority, tvainfo@tva.gov +// No copyright is claimed pursuant to 17 USC § 105. All Other Rights Reserved. +// +// Licensed under TVA Custom License based on NASA Open Source Agreement (TVA Custom NOSA); +// you may not use TVA Code Library except in compliance with the TVA Custom NOSA. You may +// obtain a copy of the TVA Custom NOSA at http://tvacodelibrary.codeplex.com/license. +// +// TVA Code Library is provided by the copyright holders and contributors "as is" and any express +// or implied warranties, including, but not limited to, the implied warranties of merchantability +// and fitness for a particular purpose are disclaimed. +// +//********************************************************************************************************************* +// +// Code Modification History: +// ------------------------------------------------------------------------------------------------------------------- +// 06/14/2012 - Stephen C. Wills, Grid Protection Alliance +// Generated original version of source code. +// +//********************************************************************************************************************* + +namespace FaultAlgorithms +{ + /// + /// Contains data for both the voltage + /// and current on a conductor. + /// + public class Conductor + { + #region [ Members ] + + // Fields + + /// + /// One cycle of voltage data. + /// + public Cycle V; + + /// + /// One cycle of current data. + /// + public Cycle I; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + public Conductor() + { + V = new Cycle(); + I = new Cycle(); + } + + /// + /// Creates a new instance of the class. + /// + /// The index of the cycle to be calculated. + /// The value to divide from the sample rate to determine the starting location of the cycle. + /// The frequency of the sine wave during this cycle. + /// The voltage data points. + /// The current data points. + public Conductor(int cycleIndex, int sampleRateDivisor, double frequency, MeasurementData voltageData, MeasurementData currentData) + { + int vStart = cycleIndex * (voltageData.SampleRate / sampleRateDivisor); + int iStart = cycleIndex * (currentData.SampleRate / sampleRateDivisor); + V = new Cycle(vStart, frequency, voltageData); + I = new Cycle(iStart, frequency, currentData); + } + + #endregion + } +} diff --git a/src/Libraries/FaultAlgorithms/Cycle.cs b/src/Libraries/FaultAlgorithms/Cycle.cs new file mode 100644 index 00000000..91db47e8 --- /dev/null +++ b/src/Libraries/FaultAlgorithms/Cycle.cs @@ -0,0 +1,200 @@ +//********************************************************************************************************************* +// Cycle.cs +// Version 1.1 and subsequent releases +// +// Copyright 2013, 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. +// +// -------------------------------------------------------------------------------------------------------------------- +// +// Version 1.0 +// +// Copyright 2012 ELECTRIC POWER RESEARCH INSTITUTE, INC. All rights reserved. +// +// openFLE ("this software") is licensed under BSD 3-Clause license. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// Redistributions of source code must retain the above copyright notice, this list of conditions and +// the following disclaimer. +// +// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and +// the following disclaimer in the documentation and/or other materials provided with the distribution. +// +// Neither the name of the Electric Power Research Institute, Inc. (EPRI) nor the names of its contributors +// may be used to endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL EPRI BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// +// This software incorporates work covered by the following copyright and permission notice: +// +// TVA Code Library 4.0.4.3 - Tennessee Valley Authority, tvainfo@tva.gov +// No copyright is claimed pursuant to 17 USC 105. All Other Rights Reserved. +// +// Licensed under TVA Custom License based on NASA Open Source Agreement (TVA Custom NOSA); +// you may not use TVA Code Library except in compliance with the TVA Custom NOSA. You may +// obtain a copy of the TVA Custom NOSA at http://tvacodelibrary.codeplex.com/license. +// +// TVA Code Library is provided by the copyright holders and contributors "as is" and any express +// or implied warranties, including, but not limited to, the implied warranties of merchantability +// and fitness for a particular purpose are disclaimed. +// +//********************************************************************************************************************* +// +// Code Modification History: +// ------------------------------------------------------------------------------------------------------------------- +// 05/23/2012 - J. Ritchie Carroll, Grid Protection Alliance +// Generated original version of source code. +// +//********************************************************************************************************************* + +using Gemstone; +using Gemstone.Numeric; +using Gemstone.Numeric.Analysis; +using Gemstone.Units; + +namespace FaultAlgorithms +{ + /// + /// Represents a cycle of single phase power frequency-domain data. + /// + public class Cycle + { + #region [ Members ] + + // Constants + private const double PiOverTwo = Math.PI / 2.0D; + + // Fields + + /// + /// The actual frequency of the cycle in hertz. + /// + public double Frequency; + + /// + /// The complex number representation of the RMS phasor. + /// + public ComplexNumber Complex; + + /// + /// The most extreme data point in the cycle. + /// + public double Peak; + + /// + /// The error between the sine fit and the given data values. + /// + public double Error; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + public Cycle() + { + } + + /// + /// Creates a new instance of the class. + /// + /// The index of the start of the cycle. + /// The frequency of the measured system, in Hz. + /// The time-domain data to be used to calculate frequency-domain values. + public Cycle(int startSample, double frequency, MeasurementData waveFormData) + { + long timeStart; + double[] timeInSeconds; + double[] measurements; + SineWave sineFit; + + if (startSample < 0) + throw new ArgumentOutOfRangeException("startSample"); + + if (startSample + waveFormData.SampleRate > waveFormData.Times.Length) + throw new ArgumentOutOfRangeException("startSample"); + + if (startSample + waveFormData.SampleRate > waveFormData.Measurements.Length) + throw new ArgumentOutOfRangeException("startSample"); + + timeStart = waveFormData.Times[startSample]; + timeInSeconds = new double[waveFormData.SampleRate]; + measurements = new double[waveFormData.SampleRate]; + + for (int i = 0; i < waveFormData.SampleRate; i++) + { + timeInSeconds[i] = Ticks.ToSeconds(waveFormData.Times[i + startSample] - timeStart); + measurements[i] = waveFormData.Measurements[i + startSample]; + } + + sineFit = WaveFit.SineFit(measurements, timeInSeconds, frequency); + + RMS = Math.Sqrt(measurements.Select(vi => vi * vi).Average()); + Phase = sineFit.Phase - PiOverTwo; + Peak = sineFit.Amplitude; + Frequency = frequency; + + Error = timeInSeconds + .Select(time => sineFit.CalculateY(time)) + .Zip(measurements, (calc, measurement) => Math.Abs(calc - measurement)) + .Sum(); + } + + #endregion + + #region [ Properties ] + + /// + /// Root-mean-square of the in the cycle. + /// + public double RMS + { + get + { + return Complex.Magnitude; + } + set + { + Complex.Magnitude = value; + } + } + + /// + /// Phase angle of the start of the cycle, relative to the reference angle. + /// + public Angle Phase + { + get + { + return Complex.Angle; + } + set + { + Complex.Angle = value; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Libraries/FaultAlgorithms/CycleData.cs b/src/Libraries/FaultAlgorithms/CycleData.cs new file mode 100644 index 00000000..610d5960 --- /dev/null +++ b/src/Libraries/FaultAlgorithms/CycleData.cs @@ -0,0 +1,177 @@ +//********************************************************************************************************************* +// CycleData.cs +// Version 1.1 and subsequent releases +// +// Copyright © 2013, 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. +// +// -------------------------------------------------------------------------------------------------------------------- +// +// Version 1.0 +// +// Copyright 2012 ELECTRIC POWER RESEARCH INSTITUTE, INC. All rights reserved. +// +// openFLE ("this software") is licensed under BSD 3-Clause license. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// • Redistributions of source code must retain the above copyright notice, this list of conditions and +// the following disclaimer. +// +// • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and +// the following disclaimer in the documentation and/or other materials provided with the distribution. +// +// • Neither the name of the Electric Power Research Institute, Inc. (“EPRI”) nor the names of its contributors +// may be used to endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL EPRI BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// +// This software incorporates work covered by the following copyright and permission notice: +// +// • TVA Code Library 4.0.4.3 - Tennessee Valley Authority, tvainfo@tva.gov +// No copyright is claimed pursuant to 17 USC § 105. All Other Rights Reserved. +// +// Licensed under TVA Custom License based on NASA Open Source Agreement (TVA Custom NOSA); +// you may not use TVA Code Library except in compliance with the TVA Custom NOSA. You may +// obtain a copy of the TVA Custom NOSA at http://tvacodelibrary.codeplex.com/license. +// +// TVA Code Library is provided by the copyright holders and contributors "as is" and any express +// or implied warranties, including, but not limited to, the implied warranties of merchantability +// and fitness for a particular purpose are disclaimed. +// +//********************************************************************************************************************* +// +// Code Modification History: +// ------------------------------------------------------------------------------------------------------------------- +// 06/14/2012 - Stephen C. Wills, Grid Protection Alliance +// Generated original version of source code. +// +//********************************************************************************************************************* + +using Gemstone.Numeric; + +namespace FaultAlgorithms +{ + /// + /// Contains data for a single cycle over all three line-to-neutral conductors. + /// + public class CycleData + { + #region [ Members ] + + // Constants + + /// + /// 2 * pi + /// + public const double TwoPI = 2.0D * Math.PI; + + // a = e^((2/3) * pi * i) + private const double Rad120 = TwoPI / 3.0D; + private static readonly ComplexNumber a = new ComplexNumber(Math.Cos(Rad120), Math.Sin(Rad120)); + private static readonly ComplexNumber aSq = a * a; + + // Fields + + /// + /// A-to-neutral conductor + /// + public Conductor AN; + + /// + /// B-to-neutral conductor + /// + public Conductor BN; + + /// + /// C-to-neutral conductor + /// + public Conductor CN; + + /// + /// Timestamp of the start of the cycle. + /// + public DateTime StartTime; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + public CycleData() + { + AN = new Conductor(); + BN = new Conductor(); + CN = new Conductor(); + } + + /// + /// Creates a new instance of the class. + /// + /// The index of the cycle being created. + /// The value to divide from the sample rate to determine the index of the sample at the start of the cycle. + /// The frequency of the measured system, in Hz. + /// The data set containing voltage measurements. + /// The data set containing current measurements. + public CycleData(int cycleIndex, int sampleRateDivisor, double frequency, MeasurementDataSet voltageDataSet, MeasurementDataSet currentDataSet) + { + int sampleIndex; + + AN = new Conductor(cycleIndex, sampleRateDivisor, frequency, voltageDataSet.AN, currentDataSet.AN); + BN = new Conductor(cycleIndex, sampleRateDivisor, frequency, voltageDataSet.BN, currentDataSet.BN); + CN = new Conductor(cycleIndex, sampleRateDivisor, frequency, voltageDataSet.CN, currentDataSet.CN); + + sampleIndex = cycleIndex * (voltageDataSet.AN.SampleRate / sampleRateDivisor); + StartTime = new DateTime(voltageDataSet.AN.Times[sampleIndex]); + } + + #endregion + + #region [ Methods ] + + /// + /// Calculates the positive, negative, and zero sequence components + /// and returns them in an array with indexes 1, 2, and 0 respectively. + /// + /// The cycle of A-to-neutral data to be used. + /// The cycle of B-to-neutral data to be used. + /// The cycle of C-to-neutral data to be used. + /// An array of size 3 containing the zero sequence, positive sequence, and negative sequence components in that order. + public static ComplexNumber[] CalculateSequenceComponents(Cycle anCycle, Cycle bnCycle, Cycle cnCycle) + { + ComplexNumber an = anCycle.Complex; + ComplexNumber bn = bnCycle.Complex; + ComplexNumber cn = cnCycle.Complex; + + ComplexNumber[] sequenceComponents = new ComplexNumber[3]; + + sequenceComponents[0] = (an + bn + cn) / 3.0D; + sequenceComponents[1] = (an + a * bn + aSq * cn) / 3.0D; + sequenceComponents[2] = (an + aSq * bn + a * cn) / 3.0D; + + return sequenceComponents; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Libraries/FaultAlgorithms/CycleDataSet.cs b/src/Libraries/FaultAlgorithms/CycleDataSet.cs new file mode 100644 index 00000000..34b810b7 --- /dev/null +++ b/src/Libraries/FaultAlgorithms/CycleDataSet.cs @@ -0,0 +1,303 @@ +//********************************************************************************************************************* +// CycleDataSet.cs +// Version 1.1 and subsequent releases +// +// Copyright © 2013, 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. +// +// -------------------------------------------------------------------------------------------------------------------- +// +// Version 1.0 +// +// Copyright 2012 ELECTRIC POWER RESEARCH INSTITUTE, INC. All rights reserved. +// +// openFLE ("this software") is licensed under BSD 3-Clause license. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// • Redistributions of source code must retain the above copyright notice, this list of conditions and +// the following disclaimer. +// +// • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and +// the following disclaimer in the documentation and/or other materials provided with the distribution. +// +// • Neither the name of the Electric Power Research Institute, Inc. (“EPRI”) nor the names of its contributors +// may be used to endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL EPRI BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// +// This software incorporates work covered by the following copyright and permission notice: +// +// • TVA Code Library 4.0.4.3 - Tennessee Valley Authority, tvainfo@tva.gov +// No copyright is claimed pursuant to 17 USC § 105. All Other Rights Reserved. +// +// Licensed under TVA Custom License based on NASA Open Source Agreement (TVA Custom NOSA); +// you may not use TVA Code Library except in compliance with the TVA Custom NOSA. You may +// obtain a copy of the TVA Custom NOSA at http://tvacodelibrary.codeplex.com/license. +// +// TVA Code Library is provided by the copyright holders and contributors "as is" and any express +// or implied warranties, including, but not limited to, the implied warranties of merchantability +// and fitness for a particular purpose are disclaimed. +// +//********************************************************************************************************************* +// +// Code Modification History: +// ------------------------------------------------------------------------------------------------------------------- +// 06/14/2012 - Stephen C. Wills, Grid Protection Alliance +// Generated original version of source code. +// +//********************************************************************************************************************* + +using System.Collections; +using Gemstone.Numeric; +using Gemstone.Numeric.Analysis; + +namespace FaultAlgorithms +{ + /// + /// Represents a collection of all the cycles extracted from a given data set. + /// + public class CycleDataSet : IEnumerable + { + #region [ Members ] + + // Fields + private List m_cycles; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + public CycleDataSet() + { + m_cycles = new List(); + } + + /// + /// Creates a new instance of the class. + /// + /// The frequency of the measured system, in Hz. + /// The data set containing voltage data points. + /// The data set containing current data points. + public CycleDataSet(double frequency, MeasurementDataSet voltageDataSet, MeasurementDataSet currentDataSet) + { + Populate(frequency, voltageDataSet, currentDataSet); + } + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets the data structure containing a + /// full cycle of data at the given index. + /// + /// The index of the cycle. + /// The cycle of data at the given index. + public CycleData this[int i] + { + get + { + return m_cycles[i]; + } + set + { + while(i >= m_cycles.Count) + m_cycles.Add(null); + + m_cycles[i] = value; + } + } + + /// + /// Gets the size of the cycle data set. + /// + public int Count + { + get + { + return m_cycles.Count; + } + } + + #endregion + + #region [ Methods ] + + /// + /// Populates the cycle data set by calculating cycle + /// data based on the given measurement data sets. + /// + /// The frequency of the measured system, in Hz. + /// Data set containing voltage waveform measurements. + /// Data set containing current waveform measurements. + public void Populate(double frequency, MeasurementDataSet voltageDataSet, MeasurementDataSet currentDataSet) + { + List measurementDataList; + int sampleRateDivisor; + int numberOfCycles; + + measurementDataList = new List() + { + voltageDataSet.AN, voltageDataSet.BN, voltageDataSet.CN, + currentDataSet.AN, currentDataSet.BN, currentDataSet.CN + }; + + sampleRateDivisor = measurementDataList + .Select(measurementData => measurementData.SampleRate) + .GreatestCommonDenominator(); + + numberOfCycles = measurementDataList + .Select(measurementData => (measurementData.Measurements.Length - measurementData.SampleRate + 1) / (measurementData.SampleRate / sampleRateDivisor)) + .Min(); + + for (int i = 0; i < numberOfCycles; i++) + m_cycles.Add(new CycleData(i, sampleRateDivisor, frequency, voltageDataSet, currentDataSet)); + } + + /// + /// Returns the index of the cycle with the largest total current. + /// + /// The index of the cycle with the largest total current. + public int GetLargestCurrentIndex() + { + int index = 0; + int bestFaultIndex = -1; + double largestCurrent = 0.0D; + + foreach (CycleData cycle in m_cycles) + { + double totalCurrent = cycle.AN.I.RMS + cycle.BN.I.RMS + cycle.CN.I.RMS; + + if (totalCurrent > largestCurrent) + { + bestFaultIndex = index; + largestCurrent = totalCurrent; + } + + index++; + } + + return bestFaultIndex; + } + + /// + /// Clears the cycle data set so that it can be repopulated. + /// + public void Clear() + { + m_cycles.Clear(); + } + + /// + /// Returns an enumerator that iterates through the collection of cycles. + /// + /// An object that can be used to iterate through the collection. + public IEnumerator GetEnumerator() + { + foreach (CycleData cycle in m_cycles) + { + yield return cycle; + } + } + + /// + /// Returns an enumerator that iterates through the collection of cycles. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #region [ Static ] + + // Static Methods + + /// + /// Exports the given to a CSV file. + /// + /// The name of the CSV file. + /// The cycle data set to be exported. + public static void ExportToCSV(string fileName, CycleDataSet cycles) + { + const string Header = + "AN V RMS,AN V Phase,AN V Peak," + + "BN V RMS,BN V Phase,BN V Peak," + + "CN V RMS,CN V Phase,CN V Peak," + + "Pos V Magnitude,Pos V Angle," + + "Neg V Magnitude,Neg V Angle," + + "Zero V Magnitude,Zero V Angle," + + "AN I RMS,AN I Phase,AN I Peak," + + "BN I RMS,BN I Phase,BN I Peak," + + "CN I RMS,CN I Phase,CN I Peak," + + "Pos I Magnitude,Pos I Angle," + + "Neg I Magnitude,Neg I Angle," + + "Zero I Magnitude,Zero I Angle"; + + using (FileStream fileStream = File.OpenWrite(fileName)) + { + using (TextWriter fileWriter = new StreamWriter(fileStream)) + { + // Write the CSV header to the file + fileWriter.WriteLine(Header); + + // Write data to the file + foreach (CycleData cycleData in cycles.m_cycles) + fileWriter.WriteLine(ToCSV(cycleData)); + } + } + } + + // Converts the cycle data to a row of CSV data. + private static string ToCSV(CycleData cycleData) + { + ComplexNumber[] vSeq = CycleData.CalculateSequenceComponents(cycleData.AN.V, cycleData.BN.V, cycleData.CN.V); + ComplexNumber[] iSeq = CycleData.CalculateSequenceComponents(cycleData.AN.I, cycleData.BN.I, cycleData.CN.I); + + string vCsv = string.Format("{0},{1},{2}", ToCSV(cycleData.AN.V), ToCSV(cycleData.BN.V), ToCSV(cycleData.CN.V)); + string vSeqCsv = string.Format("{0},{1},{2}", ToCSV(vSeq[1]), ToCSV(vSeq[2]), ToCSV(vSeq[0])); + string iCsv = string.Format("{0},{1},{2}", ToCSV(cycleData.AN.I), ToCSV(cycleData.BN.I), ToCSV(cycleData.CN.I)); + string iSeqCsv = string.Format("{0},{1},{2}", ToCSV(iSeq[1]), ToCSV(iSeq[2]), ToCSV(iSeq[0])); + + return string.Format("{0},{1},{2},{3}", vCsv, vSeqCsv, iCsv, iSeqCsv); + } + + // Converts the cycle to CSV data. + private static string ToCSV(Cycle cycle) + { + return string.Format("{0},{1},{2}", cycle.RMS, cycle.Phase.ToDegrees(), cycle.Peak); + } + + // Converts the sequence component to CSV data. + private static string ToCSV(ComplexNumber sequenceComponent) + { + return string.Format("{0},{1}", sequenceComponent.Magnitude, sequenceComponent.Angle.ToDegrees()); + } + + #endregion + } +} diff --git a/src/Libraries/FaultAlgorithms/FaultAlgorithms.csproj b/src/Libraries/FaultAlgorithms/FaultAlgorithms.csproj new file mode 100644 index 00000000..bc767802 --- /dev/null +++ b/src/Libraries/FaultAlgorithms/FaultAlgorithms.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/src/Libraries/FaultAlgorithms/MeasurementData.cs b/src/Libraries/FaultAlgorithms/MeasurementData.cs new file mode 100644 index 00000000..04da176a --- /dev/null +++ b/src/Libraries/FaultAlgorithms/MeasurementData.cs @@ -0,0 +1,97 @@ +//********************************************************************************************************************* +// MeasurementData.cs +// Version 1.1 and subsequent releases +// +// Copyright 2013, 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. +// +// -------------------------------------------------------------------------------------------------------------------- +// +// Version 1.0 +// +// Copyright 2012 ELECTRIC POWER RESEARCH INSTITUTE, INC. All rights reserved. +// +// openFLE ("this software") is licensed under BSD 3-Clause license. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// Redistributions of source code must retain the above copyright notice, this list of conditions and +// the following disclaimer. +// +// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and +// the following disclaimer in the documentation and/or other materials provided with the distribution. +// +// Neither the name of the Electric Power Research Institute, Inc. (EPRI) nor the names of its contributors +// may be used to endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL EPRI BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// +// This software incorporates work covered by the following copyright and permission notice: +// +// TVA Code Library 4.0.4.3 - Tennessee Valley Authority, tvainfo@tva.gov +// No copyright is claimed pursuant to 17 USC 105. All Other Rights Reserved. +// +// Licensed under TVA Custom License based on NASA Open Source Agreement (TVA Custom NOSA); +// you may not use TVA Code Library except in compliance with the TVA Custom NOSA. You may +// obtain a copy of the TVA Custom NOSA at http://tvacodelibrary.codeplex.com/license. +// +// TVA Code Library is provided by the copyright holders and contributors "as is" and any express +// or implied warranties, including, but not limited to, the implied warranties of merchantability +// and fitness for a particular purpose are disclaimed. +// +//********************************************************************************************************************* +// +// Code Modification History: +// ------------------------------------------------------------------------------------------------------------------- +// 05/23/2012 - J. Ritchie Carroll, Grid Protection Alliance +// Generated original version of source code. +// +//********************************************************************************************************************* + +namespace FaultAlgorithms +{ + /// + /// Represents a set of single phase power time-domain data. + /// + public class MeasurementData + { + #region [ Members ] + + // Fields + + /// + /// Array of times in ticks (100 nanosecond intervals). + /// + public long[] Times; + + /// + /// Array of measured values. + /// + public double[] Measurements; + + /// + /// The number of measured samples per cycle of data. + /// + public int SampleRate; + + #endregion + } +} \ No newline at end of file diff --git a/src/Libraries/FaultAlgorithms/MeasurementDataSet.cs b/src/Libraries/FaultAlgorithms/MeasurementDataSet.cs new file mode 100644 index 00000000..1a6c0fb0 --- /dev/null +++ b/src/Libraries/FaultAlgorithms/MeasurementDataSet.cs @@ -0,0 +1,272 @@ +//********************************************************************************************************************* +// MeasurementDataSet.cs +// Version 1.1 and subsequent releases +// +// Copyright 2013, 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. +// +// -------------------------------------------------------------------------------------------------------------------- +// +// Version 1.0 +// +// Copyright 2012 ELECTRIC POWER RESEARCH INSTITUTE, INC. All rights reserved. +// +// openFLE ("this software") is licensed under BSD 3-Clause license. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// Redistributions of source code must retain the above copyright notice, this list of conditions and +// the following disclaimer. +// +// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and +// the following disclaimer in the documentation and/or other materials provided with the distribution. +// +// Neither the name of the Electric Power Research Institute, Inc. (EPRI) nor the names of its contributors +// may be used to endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL EPRI BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// +// This software incorporates work covered by the following copyright and permission notice: +// +// TVA Code Library 4.0.4.3 - Tennessee Valley Authority, tvainfo@tva.gov +// No copyright is claimed pursuant to 17 USC 105. All Other Rights Reserved. +// +// Licensed under TVA Custom License based on NASA Open Source Agreement (TVA Custom NOSA); +// you may not use TVA Code Library except in compliance with the TVA Custom NOSA. You may +// obtain a copy of the TVA Custom NOSA at http://tvacodelibrary.codeplex.com/license. +// +// TVA Code Library is provided by the copyright holders and contributors "as is" and any express +// or implied warranties, including, but not limited to, the implied warranties of merchantability +// and fitness for a particular purpose are disclaimed. +// +//********************************************************************************************************************* +// +// Code Modification History: +// ------------------------------------------------------------------------------------------------------------------- +// 05/23/2012 - J. Ritchie Carroll, Grid Protection Alliance +// Generated original version of source code. +// +//********************************************************************************************************************* + +using Gemstone; + +namespace FaultAlgorithms +{ + /// + /// Represents a set of 3-phase line-to-neutral and line-to-line time-domain power data. + /// + public class MeasurementDataSet + { + #region [ Members ] + + // Constants + + private const string DateTimeFormat = "yyyy-MM-dd HH:mm:ss.ffffff"; + + // Fields + + /// + /// Line-to-neutral A-phase data. + /// + public MeasurementData AN; + + /// + /// Line-to-neutral B-phase data. + /// + public MeasurementData BN; + + /// + /// Line-to-neutral C-phase data. + /// + public MeasurementData CN; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new . + /// + public MeasurementDataSet() + { + AN = new MeasurementData(); + BN = new MeasurementData(); + CN = new MeasurementData(); + } + + #endregion + + #region [ Methods ] + + /// + /// Uses system frequency to calculate the sample rate for each set + /// of in this measurement data set. + /// + /// The frequency of the measured system, in Hz. + public void CalculateSampleRates(double frequency) + { + CalculateSampleRate(frequency, AN); + CalculateSampleRate(frequency, BN); + CalculateSampleRate(frequency, CN); + } + + /// + /// Explicitly sets the sample rate for each set of + /// in this measurement data set. + /// + /// The sample rate. + public void SetSampleRate(int sampleRate) + { + AN.SampleRate = sampleRate; + BN.SampleRate = sampleRate; + CN.SampleRate = sampleRate; + } + + /// + /// Writes all voltage measurement data to a CSV file. + /// + /// Export file name. + public void ExportVoltageDataToCSV(string fileName) + { + const string Header = "Time,AN,BN,CN,AB,BC,CA"; + + using (FileStream fileStream = File.OpenWrite(fileName)) + { + using (TextWriter fileWriter = new StreamWriter(fileStream)) + { + // Write the CSV header to the file + fileWriter.WriteLine(Header); + + // Write the data to the file + for (int i = 0; i < AN.Times.Length; i++) + { + string time = new DateTime(AN.Times[i]).ToString(DateTimeFormat); + + double an = AN.Measurements[i]; + double bn = BN.Measurements[i]; + double cn = CN.Measurements[i]; + + fileWriter.Write("{0},{1},{2},{3},", time, an, bn, cn); + fileWriter.WriteLine("{0},{1},{2}", an - bn, bn - cn, cn - an); + } + } + } + } + + /// + /// Writes all current measurement data to a CSV file. + /// + /// Export file name. + public void ExportCurrentDataToCSV(string fileName) + { + const string Header = "Time,AN,BN,CN"; + + using (FileStream fileStream = File.OpenWrite(fileName)) + { + using (TextWriter fileWriter = new StreamWriter(fileStream)) + { + // Write the CSV header to the file + fileWriter.WriteLine(Header); + + // Write the data to the file + for (int i = 0; i < AN.Times.Length; i++) + { + string time = new DateTime(AN.Times[i]).ToString(DateTimeFormat); + + double an = AN.Measurements[i]; + double bn = BN.Measurements[i]; + double cn = CN.Measurements[i]; + + fileWriter.WriteLine("{0},{1},{2},{3}", time, an, bn, cn); + } + } + } + } + + private void CalculateSampleRate(double frequency, MeasurementData measurementData) + { + long[] times; + long startTicks; + long endTicks; + double cycles; + + // Get the collection of measurement timestamps + times = measurementData.Times; + + // Determine the start and end time of the data set + startTicks = times[0]; + endTicks = times[times.Length - 1]; + + // Determine the number of cycles in the file, + // based on the system frequency + cycles = frequency * Ticks.ToSeconds(endTicks - startTicks); + + // Calculate the number of samples per cycle + measurementData.SampleRate = (int)Math.Round(times.Length / cycles); + } + + #endregion + + #region [ Static ] + + // Static Methods + + /// + /// Writes all measurement data to a CSV file. + /// + /// Export file name. + /// The voltage measurement data to be written to the file. + /// The current measurement data to be written to the file. + public static void ExportToCSV(string fileName, MeasurementDataSet voltageData, MeasurementDataSet currentData) + { + const string Header = "Time,AN V,BN V,CN V,AB V,BC V,CA V,AN I,BN I,CN I"; + + using (FileStream fileStream = File.Create(fileName)) + { + using (TextWriter fileWriter = new StreamWriter(fileStream)) + { + // Write the CSV header to the file + fileWriter.WriteLine(Header); + + // Write the data to the file + for (int i = 0; i < voltageData.AN.Times.Length; i++) + { + string time = new DateTime(voltageData.AN.Times[i]).ToString(DateTimeFormat); + + double vAN = voltageData.AN.Measurements[i]; + double vBN = voltageData.BN.Measurements[i]; + double vCN = voltageData.CN.Measurements[i]; + + double iAN = currentData.AN.Measurements[i]; + double iBN = currentData.BN.Measurements[i]; + double iCN = currentData.CN.Measurements[i]; + + fileWriter.Write("{0},{1},{2},{3},", time, vAN, vBN, vCN); + fileWriter.Write("{0},{1},{2},", vAN - vBN, vBN - vCN, vCN - vAN); + fileWriter.WriteLine("{0},{1},{2}", iAN, iBN, iCN); + } + } + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Libraries/FaultData/DataAnalysis/CycleDataGroup.cs b/src/Libraries/FaultData/DataAnalysis/CycleDataGroup.cs new file mode 100644 index 00000000..3129dd76 --- /dev/null +++ b/src/Libraries/FaultData/DataAnalysis/CycleDataGroup.cs @@ -0,0 +1,115 @@ +//****************************************************************************************************** +// CycleDataGroup.cs - Gbtc +// +// Copyright © 2014, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2014 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using openXDA.Model; + +namespace FaultData.DataAnalysis +{ + public class CycleDataGroup + { + #region [ Members ] + + // Constants + private const int RMSIndex = 0; + private const int PhaseIndex = 1; + private const int PeakIndex = 2; + private const int ErrorIndex = 3; + private Asset m_asset; + // Fields + private DataGroup m_dataGroup; + + #endregion + + #region [ Constructors ] + + public CycleDataGroup(DataGroup dataGroup, Asset asset) + { + m_dataGroup = dataGroup; + m_asset = asset; + } + + #endregion + + #region [ Properties ] + + public DataSeries RMS + { + get + { + return m_dataGroup[RMSIndex]; + } + } + + public DataSeries Phase + { + get + { + return m_dataGroup[PhaseIndex]; + } + } + + public DataSeries Peak + { + get + { + return m_dataGroup[PeakIndex]; + } + } + + public DataSeries Error + { + get + { + return m_dataGroup[ErrorIndex]; + } + } + + public Asset Asset + { + get + { + return m_asset; + } + } + #endregion + + #region [ Methods ] + + public DataGroup ToDataGroup() + { + return m_dataGroup; + } + + public CycleDataGroup ToSubGroup(int startIndex, int endIndex) + { + return new CycleDataGroup(m_dataGroup.ToSubGroup(startIndex, endIndex), m_asset); + } + + public CycleDataGroup ToSubGroup(DateTime startTime, DateTime endTime) + { + return new CycleDataGroup(m_dataGroup.ToSubGroup(startTime, endTime), m_asset); + } + + #endregion + } +} diff --git a/src/Libraries/FaultData/DataAnalysis/DataGroup.cs b/src/Libraries/FaultData/DataAnalysis/DataGroup.cs new file mode 100644 index 00000000..20d91b75 --- /dev/null +++ b/src/Libraries/FaultData/DataAnalysis/DataGroup.cs @@ -0,0 +1,662 @@ +//****************************************************************************************************** +// DataGroup.cs - Gbtc +// +// Copyright © 2014, 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: +// ---------------------------------------------------------------------------------------------------- +// 05/19/2014 - Stephen C. Wills +// Generated original version of source code. +// 12/23/2019 - C. Lackner +// Adjusted to read data from blob for each dataseries. +// +//****************************************************************************************************** + +using System.Data; +using Gemstone; +using Gemstone.Data.Model; +using Ionic.Zlib; +using Microsoft.Data.SqlClient; +using openXDA.Model; + +namespace FaultData.DataAnalysis +{ + public enum DataClassification + { + Trend, + Event, + FastRMS, + Unknown + } + + public class DataGroup + { + #region [ Members ] + + // Constants + + /// + /// Maximum sample rate, in samples per minute, of data classified as . + /// + public const double TrendThreshold = 1.0D; + + // Fields + private Asset m_asset; + private DateTime m_startTime; + private DateTime m_endTime; + private int m_samples; + + private List m_dataSeries; + private List m_disturbances; + private DataClassification m_classification; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + public DataGroup() + { + m_dataSeries = new List(); + m_disturbances = new List(); + m_classification = DataClassification.Unknown; + m_asset = null; + } + + /// + /// Creates a new instance of the class. + /// + /// Asset associated with this datagroup + public DataGroup(Asset asset) + { + m_dataSeries = new List(); + m_disturbances = new List(); + m_classification = DataClassification.Unknown; + m_asset = asset; + } + + /// + /// Creates a new instance of the class. + /// + /// Collection of data series to be added to the data group. + public DataGroup(IEnumerable dataSeries) + : this() + { + foreach (DataSeries series in dataSeries) + Add(series); + } + + /// + /// Creates a new instance of the class. + /// + /// Collection of data series to be added to the data group. + /// Asset associated with this datagroup + public DataGroup(IEnumerable dataSeries, Asset asset) + : this(asset) + { + foreach (DataSeries series in dataSeries) + Add(series); + } + + #endregion + + #region [ Properties ] + + /// + /// Gets the line from which measurements were taken to create the group of data. + /// + public Asset Asset + { + get + { + return m_asset; + } + } + + /// + /// Gets the start time of the group of data. + /// + public DateTime StartTime + { + get + { + return m_startTime; + } + } + + /// + /// Gets the end time of the group of data. + /// + public DateTime EndTime + { + get + { + return m_endTime; + } + } + + /// + /// Gets the number of samples in each series. + /// + public int Samples + { + get + { + return m_samples; + } + } + + /// + /// Gets the sample rate, in samples per second, + /// of the data series in this data group. + /// + public double SamplesPerSecond + { + get + { + if (!m_dataSeries.Any()) + return double.NaN; + + return m_dataSeries[0].SampleRate; + } + } + + /// + /// Gets the duration, in seconds, + /// of the data series in this data group. + /// + public double Duration + { + get + { + if (!m_dataSeries.Any()) + return double.NaN; + + return m_dataSeries[0].Duration; + } + } + + /// + /// Gets the sample rate, in samples per hour, + /// of the data series in this data group. + /// + public double SamplesPerHour + { + get + { + return (m_samples - 1) / (m_endTime - m_startTime).TotalHours; + } + } + + /// + /// Gets flag that indicates whether the data series + /// in this data group are marked as trend channels. + /// + public bool Trend => + m_dataSeries.Any(dataSeries => dataSeries.SeriesInfo?.Channel.Trend == true); + + /// + /// Gets the channels contained in this data group. + /// + public IReadOnlyList DataSeries + { + get + { + return m_dataSeries.AsReadOnly(); + } + } + + /// + /// Gets the disturbances contained in this data group. + /// + public IReadOnlyList Disturbances + { + get + { + return m_disturbances.AsReadOnly(); + } + } + + /// + /// Gets the classification of this group of data as of the last call to . + /// + public DataClassification Classification + { + get + { + if (m_classification == DataClassification.Unknown) + Classify(); + + return m_classification; + } + } + + public DataSeries this[int index] + { + get + { + return m_dataSeries[index]; + } + } + + #endregion + + #region [ Methods ] + + /// + /// Adds a channel to the group of data. + /// + /// The channel to be added to the group. + /// + /// True if the channel was successfully added. False if the channel was excluded + /// because the channel does not match the other channels already in the data group. + /// + public bool Add(DataSeries dataSeries) + { + Asset asset; + DateTime startTime; + DateTime endTime; + int samples; + bool trend; + + // Unable to add null data series + if ((object)dataSeries == null) + return false; + + // Data series without data is irrelevant to data grouping + if (!dataSeries.DataPoints.Any()) + return false; + + // Do not add the same data series twice + if (m_dataSeries.Contains(dataSeries)) + return false; + + // Get information about the line this data is associated with + if ((object)dataSeries.SeriesInfo != null) + asset = dataSeries.SeriesInfo.Channel.Asset; + else + asset = null; + + // Get the start time, end time, number of samples, and + // trend flag for the data series passed into this function + startTime = dataSeries.DataPoints[0].Time; + endTime = dataSeries.DataPoints[dataSeries.DataPoints.Count - 1].Time; + samples = dataSeries.DataPoints.Count; + trend = dataSeries.SeriesInfo?.Channel.Trend == true; + + // If there are any disturbances in this data group that do not overlap + // with the data series, do not include the data series in the data group + if (m_disturbances.Select(disturbance => disturbance.ToRange()).Any(range => range.Start > endTime || range.End < startTime)) + return false; + + // If there are any disturbances associated with the data in this group and the data + // to be added is trending data, do not include the trending data in the data group + if (m_disturbances.Any() && CalculateSamplesPerMinute(startTime, endTime, samples) <= TrendThreshold) + return false; + + // At this point, if there is no existing data in the data + // group, add the data as the first series in the data group + if (m_dataSeries.Count == 0) + { + if (m_asset == null) + { + m_asset = asset; + } + m_startTime = startTime; + m_endTime = endTime; + m_samples = samples; + + m_dataSeries.Add(dataSeries); + m_classification = DataClassification.Unknown; + + return true; + } + + // If the data being added matches the parameters for this data group, add the data to the data group + // Note that it does not have to match Asset + if (startTime == m_startTime && endTime == m_endTime && samples == m_samples && trend == Trend) + { + m_dataSeries.Add(dataSeries); + return true; + } + + return false; + } + + /// + /// Adds a disturbance to the group of data. + /// + /// The disturbance to be added to the group. + /// True if the disturbance was successfully added. + public bool Add(ReportedDisturbance disturbance) + { + // Unable to add null disturbance + if ((object)disturbance == null) + return false; + + // Do not add the same disturbance twice + if (m_disturbances.Contains(disturbance)) + return false; + + // If the data in this data group is trending data, + // do not add the disturbance to the data group + if (Classification == DataClassification.Trend) + return false; + + // Get the start time and end time of the disturbance. + DateTime startTime = disturbance.Time; + DateTime endTime = startTime + disturbance.Duration; + + // If there are no data series and no other disturbances, + // make this the first piece of data to be added to the data group + if (!m_dataSeries.Any() && !m_disturbances.Any()) + { + m_startTime = startTime; + m_endTime = endTime; + m_disturbances.Add(disturbance); + m_classification = DataClassification.Event; + return true; + } + + // If the disturbance overlaps with + // this data group, add the disturbance + if (startTime <= m_endTime && m_startTime <= endTime) + { + // If the only data in the data group is disturbances, + // adjust the start time and end time + if (!m_dataSeries.Any() && startTime < m_startTime) + m_startTime = startTime; + + if (!m_dataSeries.Any() && endTime > m_endTime) + m_endTime = endTime; + + m_disturbances.Add(disturbance); + return true; + } + + return false; + } + + /// + /// Removes a channel from the data group. + /// + /// The channel to be removed from the data group. + /// True if the channel existed in the group and was removed; false otherwise. + public bool Remove(DataSeries dataSeries) + { + if (m_dataSeries.Remove(dataSeries)) + { + m_classification = m_disturbances.Any() + ? DataClassification.Event + : DataClassification.Unknown; + + return true; + } + + return false; + } + + /// + /// Removes a disturbance from the data group. + /// + /// THe disturbance to be removed from the data group. + /// True if the disturbance existed in the group and was removed; false otherwise. + public bool Remove(ReportedDisturbance disturbance) + { + if (m_disturbances.Remove(disturbance)) + { + if (!m_disturbances.Any()) + m_classification = DataClassification.Unknown; + + return true; + } + + return false; + } + + public DataGroup ToSubGroup(int startIndex, int endIndex) + { + DataGroup subGroup = new DataGroup(); + + foreach (DataSeries dataSeries in m_dataSeries) + subGroup.Add(dataSeries.ToSubSeries(startIndex, endIndex)); + + return subGroup; + } + + public DataGroup ToSubGroup(DateTime startTime, DateTime endTime) + { + DataGroup subGroup = new DataGroup(); + + foreach (DataSeries dataSeries in m_dataSeries) + subGroup.Add(dataSeries.ToSubSeries(startTime, endTime)); + + return subGroup; + } + + // Overwrite To Data to save Data into ChannelBlob instead of File Blob + // This needs to be done to avoid data duplication + public Dictionary ToData() + { + Dictionary result = new Dictionary(); + + var timeSeries = m_dataSeries[0].DataPoints + .Select(dataPoint => new { Time = dataPoint.Time.Ticks, Compressed = false }) + .ToList(); + + for (int i = 1; i < timeSeries.Count; i++) + { + long previousTimestamp = m_dataSeries[0][i - 1].Time.Ticks; + long timestamp = timeSeries[i].Time; + long diff = timestamp - previousTimestamp; + + if (diff >= 0 && diff <= ushort.MaxValue) + timeSeries[i] = new { Time = diff, Compressed = true }; + + + } + + int timeSeriesByteLength = timeSeries.Sum(obj => obj.Compressed ? sizeof(ushort) : sizeof(int) + sizeof(long)); + int dataSeriesByteLength = sizeof(int) + (2 * sizeof(double)) + (m_samples * sizeof(ushort)); + int totalByteLength = sizeof(int) + timeSeriesByteLength + dataSeriesByteLength; + + foreach (DataSeries dataSeries in m_dataSeries) + { + byte[] data = new byte[totalByteLength]; + int offset = 0; + + offset += LittleEndian.CopyBytes(m_samples, data, offset); + + List uncompressedIndexes = timeSeries + .Select((obj, Index) => new { obj.Compressed, Index }) + .Where(obj => !obj.Compressed) + .Select(obj => obj.Index) + .ToList(); + + for (int i = 0; i < uncompressedIndexes.Count; i++) + { + int index = uncompressedIndexes[i]; + int nextIndex = (i + 1 < uncompressedIndexes.Count) ? uncompressedIndexes[i + 1] : timeSeries.Count; + + offset += LittleEndian.CopyBytes(nextIndex - index, data, offset); + offset += LittleEndian.CopyBytes(timeSeries[index].Time, data, offset); + + for (int j = index + 1; j < nextIndex; j++) + offset += LittleEndian.CopyBytes((ushort)timeSeries[j].Time, data, offset); + } + + + if (dataSeries.Calculated) continue; + + const ushort NaNValue = ushort.MaxValue; + const ushort MaxCompressedValue = ushort.MaxValue - 1; + int seriesID = dataSeries.SeriesInfo?.ID ?? 0; + double range = dataSeries.Maximum - dataSeries.Minimum; + double decompressionOffset = dataSeries.Minimum; + double decompressionScale = range / MaxCompressedValue; + double compressionScale = (decompressionScale != 0.0D) ? 1.0D / decompressionScale : 0.0D; + + offset += LittleEndian.CopyBytes(seriesID, data, offset); + offset += LittleEndian.CopyBytes(decompressionOffset, data, offset); + offset += LittleEndian.CopyBytes(decompressionScale, data, offset); + + foreach (DataPoint dataPoint in dataSeries.DataPoints) + { + ushort compressedValue = (ushort)Math.Round((dataPoint.Value - decompressionOffset) * compressionScale); + + if (compressedValue == NaNValue) + compressedValue--; + + if (double.IsNaN(dataPoint.Value)) + compressedValue = NaNValue; + + offset += LittleEndian.CopyBytes(compressedValue, data, offset); + } + byte[] returnArray = GZipStream.CompressBuffer(data); + returnArray[0] = 0x44; + returnArray[1] = 0x33; + + int dataSeriesID = dataSeries.SeriesInfo?.ID ?? 0; + result.Add(dataSeriesID, returnArray); + } + + return result ; + } + + public void FromData(List data) + { + FromData(null, data); + } + + public void FromData(Meter meter, List dataList) + { + var decompressed = dataList.SelectMany(d => ChannelData.Decompress(d)); + + foreach (Tuple> tuple in decompressed) + { + DataSeries dataSeries = new DataSeries(); + + if (tuple.Item1 > 0 && !(meter is null)) + dataSeries.SeriesInfo = meter.Series.FirstOrDefault(s => s.ID == tuple.Item1); + + dataSeries.DataPoints = tuple.Item2; + + Add(dataSeries); + } + } + + private void Classify() + { + if (IsTrend()) + m_classification = DataClassification.Trend; + else if (IsEvent()) + m_classification = DataClassification.Event; + else if (IsFastRMS()) + m_classification = DataClassification.FastRMS; + else + m_classification = DataClassification.Unknown; + } + + private bool IsTrend() + { + if (!m_dataSeries.Any() || m_disturbances.Any()) + return false; + + double samplesPerMinute = CalculateSamplesPerMinute(m_startTime, m_endTime, m_samples); + return samplesPerMinute <= TrendThreshold; + } + + private bool IsEvent() + { + if (m_disturbances.Any()) + return true; + + return m_dataSeries + .Where(dataSeries => (object)dataSeries.SeriesInfo != null) + .Where(IsInstantaneous) + .Where(dataSeries => dataSeries.SeriesInfo.Channel.MeasurementType.Name != "Digital") + .Any(); + } + + private bool IsInstantaneous(DataSeries dataSeries) + { + string characteristicName = dataSeries.SeriesInfo.Channel.MeasurementCharacteristic.Name; + string seriesTypeName = dataSeries.SeriesInfo.SeriesType.Name; + + return (characteristicName == "Instantaneous") && + (seriesTypeName == "Values" || seriesTypeName == "Instantaneous"); + } + + private bool IsFastRMS() + { + return m_dataSeries + .Where(dataSeries => (object)dataSeries.SeriesInfo != null) + .Where(IsRMS) + .Any(); + } + + private bool IsRMS(DataSeries dataSeries) + { + string characteristicName = dataSeries.SeriesInfo.Channel.MeasurementCharacteristic.Name; + string seriesTypeName = dataSeries.SeriesInfo.SeriesType.Name; + + return (characteristicName == "RMS") && + (seriesTypeName == "Values" || seriesTypeName == "Instantaneous"); + } + + private double CalculateSamplesPerMinute(DateTime startTime, DateTime endTime, int samples) + { + return (samples - 1) / (endTime - startTime).TotalMinutes; + } + + #endregion + } + + public static partial class TableOperationsExtensions + { + public static Event GetEvent(this TableOperations eventTable, FileGroup fileGroup, DataGroup dataGroup) + { + int fileGroupID = fileGroup.ID; + int assetID = dataGroup.Asset.ID; + DateTime startTime = dataGroup.StartTime; + DateTime endTime = dataGroup.EndTime; + int samples = dataGroup.Samples; + + IDbDataParameter startTimeParameter = new SqlParameter() + { + ParameterName = nameof(dataGroup.StartTime), + DbType = DbType.DateTime2, + Value = startTime + }; + + IDbDataParameter endTimeParameter = new SqlParameter() + { + ParameterName = nameof(dataGroup.EndTime), + DbType = DbType.DateTime2, + Value = endTime + }; + + RecordRestriction recordRestriction = + new RecordRestriction("FileGroupID = {0}", fileGroupID) & + new RecordRestriction("AssetID = {0}", assetID) & + new RecordRestriction("StartTime = {0}", startTimeParameter) & + new RecordRestriction("EndTime = {0}", endTimeParameter) & + new RecordRestriction("Samples = {0}", samples); + + return eventTable.QueryRecord(recordRestriction); + } + } +} diff --git a/src/Libraries/FaultData/DataAnalysis/DataSeries.cs b/src/Libraries/FaultData/DataAnalysis/DataSeries.cs new file mode 100644 index 00000000..cbc7d9d1 --- /dev/null +++ b/src/Libraries/FaultData/DataAnalysis/DataSeries.cs @@ -0,0 +1,588 @@ +//****************************************************************************************************** +// DataSeries.cs - Gbtc +// +// Copyright © 2014, 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: +// ---------------------------------------------------------------------------------------------------- +// 05/15/2014 - Stephen C. Wills +// Generated original version of source code. +// 07/09/2019 - Christoph Lackner +// Added length property and Threshhold method. +// +//****************************************************************************************************** + +using Gemstone; +using Gemstone.Numeric.Interpolation; +using Ionic.Zlib; +using openXDA.Model; + +namespace FaultData.DataAnalysis +{ + /// + /// Represents a series of data points. + /// + public class DataSeries + { + #region [ Members ] + + // Fields + private Series m_seriesInfo; + private List m_dataPoints; + + private double? m_duration; + private double? m_sampleRate; + private double? m_minimum; + private double? m_maximum; + private double? m_average; + + #endregion + + #region [ Constructors ] + + public DataSeries() + { + m_dataPoints = new List(); + } + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets the configuration information + /// that defines the data in this series. + /// + public Series SeriesInfo + { + get + { + return m_seriesInfo; + } + set + { + m_seriesInfo = value; + } + } + + /// + /// Gets or sets the data points that make up the series. + /// + public List DataPoints + { + get + { + return m_dataPoints; + } + set + { + m_dataPoints = value ?? new List(); + m_duration = null; + m_sampleRate = null; + m_minimum = null; + m_maximum = null; + m_average = null; + } + } + + /// + /// Gets the duration of the series, in seconds. + /// + public double Duration + { + get + { + if (m_duration.HasValue) + return m_duration.Value; + + if (!m_dataPoints.Any()) + return double.NaN; + + m_duration = m_dataPoints.Last().Time.Subtract(m_dataPoints.First().Time).TotalSeconds; + + return m_duration.Value; + } + } + + /// + /// Gets the Start Time of the dataseries. + /// + public DateTime StartTime + { + get + { + if (!m_dataPoints.Any()) + return DateTime.MinValue; + return m_dataPoints.First().Time; + } + } + + /// + /// Gets the End Time of the dataseries. + /// + public DateTime EndTime + { + get + { + if (!m_dataPoints.Any()) + return DateTime.MinValue; + return m_dataPoints.Last().Time; + } + } + + + /// + /// Gets the Length of the series, in datapoints. + /// + public int Length + { + get + { + + if (!m_dataPoints.Any()) + return 0; + + return m_dataPoints.Count; + } + } + + /// + /// Gets the sample rate of the series, in samples per second. + /// + public double SampleRate + { + get + { + if (m_sampleRate.HasValue) + return m_sampleRate.Value; + + if (!m_dataPoints.Any()) + return double.NaN; + + int index = (m_dataPoints.Count > 128) ? 128 : m_dataPoints.Count - 1; + + m_sampleRate = (Duration != 0.0D) + ? index / (m_dataPoints[index].Time - m_dataPoints[0].Time).TotalSeconds + : double.NaN; + + return m_sampleRate.Value; + } + } + + /// + /// Gets the maximum value in the series. + /// + public double Maximum + { + get + { + if (m_maximum.HasValue) + return m_maximum.Value; + + if (!m_dataPoints.Any(dataPoint => !double.IsNaN(dataPoint.Value))) + return double.NaN; + + m_maximum = m_dataPoints + .Select(point => point.Value) + .Where(value => !double.IsNaN(value)) + .Max(); + + return m_maximum.Value; + } + } + + /// + /// Gets the minimum value in the series. + /// + public double Minimum + { + get + { + if (m_minimum.HasValue) + return m_minimum.Value; + + if (!m_dataPoints.Any(dataPoint => !double.IsNaN(dataPoint.Value))) + return double.NaN; + + m_minimum = m_dataPoints + .Select(dataPoint => dataPoint.Value) + .Where(value => !double.IsNaN(value)) + .Min(); + + return m_minimum.Value; + } + } + + /// + /// Gets the average value in the series. + /// + public double Average + { + get + { + if (m_average.HasValue) + return m_average.Value; + + if (!m_dataPoints.Any(dataPoint => !double.IsNaN(dataPoint.Value))) + return double.NaN; + + m_average = m_dataPoints + .Select(dataPoint => dataPoint.Value) + .Where(value => !double.IsNaN(value)) + .Average(); + + return m_average.Value; + } + } + + public DataPoint this[int index] + { + get + { + return m_dataPoints[index]; + } + } + + /// + /// Flag that tells the DataGroup .ToData function not to add to data blob because this value is calculated. + /// + public bool Calculated { get; set; } = false; + + #endregion + + #region [ Methods ] + + + /// + /// Creates a new that is a subset. + /// + /// The index at which the new DataSeries starts. + /// The index at which the new DataSeries ends. + /// a new + public DataSeries ToSubSeries(int startIndex, int endIndex) + { + DataSeries subSeries = new DataSeries(); + int count; + + subSeries.SeriesInfo = m_seriesInfo; + + if (startIndex < 0) + startIndex = 0; + + if (endIndex >= m_dataPoints.Count) + endIndex = m_dataPoints.Count - 1; + + count = endIndex - startIndex + 1; + + if (count > 0) + subSeries.DataPoints = m_dataPoints.Skip(startIndex).Take(count).ToList(); + + return subSeries; + } + + /// + /// Creates a new that is a subset. + /// + /// The index at which the new DataSeries starts. + /// a new + public DataSeries ToSubSeries(int startSeries) => ToSubSeries(startSeries, this.Length); + + public DataSeries ToSubSeries(DateTime startTime, DateTime endTime) + { + DataSeries subSeries = new DataSeries(); + + subSeries.SeriesInfo = m_seriesInfo; + + subSeries.DataPoints = m_dataPoints + .SkipWhile(point => point.Time < startTime) + .TakeWhile(point => point.Time <= endTime) + .ToList(); + + return subSeries; + } + + /// + /// Creates a new that is a subset. + /// + /// The time at which the new DataSeries starts. + /// a new + public DataSeries ToSubSeries(DateTime startTime) => ToSubSeries(startTime, this[this.Length - 1].Time); + + public DataSeries Shift(TimeSpan timeShift) + { + DataSeries shifted = new DataSeries(); + + shifted.SeriesInfo = m_seriesInfo; + + shifted.DataPoints = m_dataPoints + .Select(dataPoint => dataPoint.Shift(timeShift)) + .ToList(); + + return shifted; + } + + public DataSeries Negate() + { + DataSeries negatedDataSeries = new DataSeries(); + + negatedDataSeries.DataPoints = m_dataPoints + .Select(point => point.Negate()) + .ToList(); + + return negatedDataSeries; + } + + public DataSeries Add(DataSeries operand) + { + DataSeries sum = new DataSeries(); + + if (m_dataPoints.Count != operand.DataPoints.Count) + throw new InvalidOperationException("Cannot take the sum of series with mismatched time values"); + + sum.DataPoints = m_dataPoints + .Zip(operand.DataPoints, Add) + .ToList(); + + return sum; + } + + public DataSeries Subtract(DataSeries operand) + { + return Add(operand.Negate()); + } + + public DataSeries Multiply(double value) + { + DataSeries result = new DataSeries(); + + result.DataPoints = m_dataPoints + .Select(point => point.Multiply(value)) + .ToList(); + + return result; + } + + public DataSeries Copy() + { + return Multiply(1.0D); + } + + public int Threshhold(double value) + { + return m_dataPoints.FindIndex(x => x.LargerThan(value)); + } + + /// + /// Downsamples the current DataSeries to requested sample count, if the + /// + /// + public void Downsample(int maxSampleCount) + { + // don't actually downsample, if it doesn't need it. + if (DataPoints.Count <= maxSampleCount) return; + + DateTime epoch = new DateTime(1970, 1, 1); + double startTime = StartTime.Subtract(epoch).TotalMilliseconds; + double endTime = EndTime.Subtract(epoch).TotalMilliseconds; + List data = new List(); + + // milliseconds per returned sampled size + int step = (int)(Duration*1000) / maxSampleCount; + if (step < 1) + step = 1; + + int index = 0; + for (double n = startTime * 1000; n <= endTime * 1000; n += 2 * step) + { + DataPoint min = null; + DataPoint max = null; + + while (index < DataPoints.Count() && DataPoints[index].Time.Subtract(epoch).TotalMilliseconds * 1000 < n + 2 * step) + { + if (min == null || min.Value > DataPoints[index].Value) + min = DataPoints[index]; + + if (max == null || max.Value <= DataPoints[index].Value) + max = DataPoints[index]; + + ++index; + } + + if (min != null) + { + if (min.Time < max.Time) + { + data.Add(min); + data.Add(max); + } + else if (min.Time > max.Time) + { + data.Add(max); + data.Add(min); + } + else + { + data.Add(min); + } + } + } + DataPoints = data; + } + + /// + /// Upsamples the current DataSeries to requested sample count, assuming the requested rate is larger than the current + /// + /// + public void Upsample(int minSamplesPerCycle, double systemFrequency) + { + // don't actually upsample, if it doesn't need it. + if (minSamplesPerCycle <= 0) + return; + TimeSpan duration = EndTime - StartTime; + double cycles = duration.TotalSeconds * systemFrequency; + int minSampleCount = (int)Math.Round(cycles * minSamplesPerCycle); + if (minSampleCount <= DataPoints.Count) + return; + + // Creating spline fit to perform upsampling + List xValues = DataPoints + .Select(point => (double) point.Time.Subtract(StartTime).Ticks) + .ToList(); + List yValues= DataPoints + .Select(point => point.Value) + .ToList(); + SplineFit splineFit = SplineFit.ComputeCubicSplines(xValues, yValues); + + List data = Enumerable + .Range(0, minSampleCount) + .Select(sample => sample * duration.Ticks / minSampleCount) + .Select(sampleTicks => + new DataPoint() + { + Time = StartTime.AddTicks(sampleTicks), + Value = splineFit.CalculateY(sampleTicks) + } + ).ToList(); + + DataPoints = data; + } + + #endregion + + #region [ Static ] + + // Static Methods + + public static DataSeries Merge(IEnumerable dataSeriesList) + { + if (dataSeriesList == null) + throw new ArgumentNullException(nameof(dataSeriesList)); + + DataSeries mergedSeries = new DataSeries(); + DateTime lastTime = default(DateTime); + + IEnumerable dataPoints = dataSeriesList + .Where(dataSeries => dataSeries != null) + .Where(dataSeries => dataSeries.DataPoints.Count != 0) + .OrderBy(dataSeries => dataSeries[0].Time) + .SelectMany(series => series.DataPoints); + + foreach (DataPoint next in dataPoints) + { + if (mergedSeries.DataPoints.Count == 0 || next.Time > lastTime) + { + mergedSeries.DataPoints.Add(next); + lastTime = next.Time; + } + } + + return mergedSeries; + } + + private static DataPoint Add(DataPoint point1, DataPoint point2) + { + return point1.Add(point2); + } + + public static DataSeries FromData(Meter meter, byte[] data) + { + + if (data == null) + return null; + + // Restore the GZip header before uncompressing + data[0] = 0x1F; + data[1] = 0x8B; + + byte[] uncompressedData = GZipStream.UncompressBuffer(data); + int offset = 0; + + int samples = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + List times = new List(); + + while (times.Count < samples) + { + int timeValues = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + long currentValue = LittleEndian.ToInt64(uncompressedData, offset); + offset += sizeof(long); + times.Add(new DateTime(currentValue)); + + for (int i = 1; i < timeValues; i++) + { + currentValue += LittleEndian.ToUInt16(uncompressedData, offset); + offset += sizeof(ushort); + times.Add(new DateTime(currentValue)); + } + } + + DataSeries dataSeries = new DataSeries(); + int seriesID = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + if (seriesID > 0 && !(meter is null)) + dataSeries.SeriesInfo = meter.Series.FirstOrDefault(s => s.ID == seriesID); + + const ushort NaNValue = ushort.MaxValue; + double decompressionOffset = LittleEndian.ToDouble(uncompressedData, offset); + double decompressionScale = LittleEndian.ToDouble(uncompressedData, offset + sizeof(double)); + offset += 2 * sizeof(double); + + for (int i = 0; i < samples; i++) + { + ushort compressedValue = LittleEndian.ToUInt16(uncompressedData, offset); + offset += sizeof(ushort); + + double decompressedValue = decompressionScale * compressedValue + decompressionOffset; + + if (compressedValue == NaNValue) + decompressedValue = double.NaN; + + dataSeries.DataPoints.Add(new DataPoint() + { + Time = times[i], + Value = decompressedValue + }); + } + + return dataSeries; + + } + + #endregion + } +} diff --git a/src/Libraries/FaultData/DataAnalysis/ReportedDisturbance.cs b/src/Libraries/FaultData/DataAnalysis/ReportedDisturbance.cs new file mode 100644 index 00000000..a078eb5b --- /dev/null +++ b/src/Libraries/FaultData/DataAnalysis/ReportedDisturbance.cs @@ -0,0 +1,58 @@ +//****************************************************************************************************** +// ReportedDisturbance.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 12/06/2017 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone; +using Gemstone.PQDIF.Logical; + +namespace FaultData.DataAnalysis +{ + public class ReportedDisturbance + { + public ReportedDisturbance(Phase phase, DateTime time, double max, double min, double avg, TimeSpan duration, QuantityUnits units) + { + Phase = phase; + Time = time; + Maximum = max; + Minimum = min; + Average = avg; + Duration = duration; + Units = units; + } + + public Phase Phase { get; } + public DateTime Time { get; } + public double Maximum { get; } + public double Minimum { get; } + public double Average { get; } + public TimeSpan Duration { get; } + public QuantityUnits Units { get; } + + public ReportedDisturbance ShiftTimestampTo(DateTime shiftedTime) => + new ReportedDisturbance(Phase, shiftedTime, Maximum, Minimum, Average, Duration, Units); + + public Range ToRange() + { + return new Range(Time, Time + Duration); + } + } +} diff --git a/src/Libraries/FaultData/DataAnalysis/Transform.cs b/src/Libraries/FaultData/DataAnalysis/Transform.cs new file mode 100644 index 00000000..9e4aa28d --- /dev/null +++ b/src/Libraries/FaultData/DataAnalysis/Transform.cs @@ -0,0 +1,358 @@ +//****************************************************************************************************** +// Transform.cs - Gbtc +// +// Copyright © 2014, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/28/2014 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Numeric.Analysis; +using openXDA.Model; + +namespace FaultData.DataAnalysis +{ + public static class Transform + { + public static DataGroup Combine(params DataGroup[] dataGroups) + { + DataGroup combination = new DataGroup(); + + foreach (DataGroup dataGroup in dataGroups) + { + foreach (DataSeries dataSeries in dataGroup.DataSeries) + combination.Add(dataSeries); + } + + return combination; + } + + public static VICycleDataGroup ToVICycleDataGroup(VIDataGroup dataGroup, double frequency, bool compress = false) + { + DataSeries[] cycleSeries = dataGroup.Data; + + return new VICycleDataGroup(cycleSeries + .Where(dataSeries => (object)dataSeries != null) + .Select(dataSeries => ToCycleDataGroup(dataSeries, frequency, compress)) + .ToList(), dataGroup.Asset); + } + + public static CycleDataGroup ToCycleDataGroup(DataSeries dataSeries, double frequency, bool compress=false) + { + if (dataSeries is null) + return null; + + DataSeries rmsSeries = new DataSeries(); + DataSeries phaseSeries = new DataSeries(); + DataSeries peakSeries = new DataSeries(); + DataSeries errorSeries = new DataSeries(); + + // Set series info to the source series info + rmsSeries.SeriesInfo = dataSeries.SeriesInfo; + phaseSeries.SeriesInfo = dataSeries.SeriesInfo; + peakSeries.SeriesInfo = dataSeries.SeriesInfo; + errorSeries.SeriesInfo = dataSeries.SeriesInfo; + + // Get samples per cycle of the data series based on the given frequency + int samplesPerCycle = CalculateSamplesPerCycle(dataSeries, frequency); + + //preinitialize size of SeriesInfo + int ncycleData = dataSeries.DataPoints.Count - samplesPerCycle + 1; + + if (ncycleData <= 0) + return null; + + rmsSeries.DataPoints.Capacity = ncycleData; + phaseSeries.DataPoints.Capacity = ncycleData; + peakSeries.DataPoints.Capacity = ncycleData; + errorSeries.DataPoints.Capacity = ncycleData; + + // Initialize arrays of y-values and t-values for calculating cycle data as necessary + double[] yValues = new double[samplesPerCycle]; + double[] tValues = new double[samplesPerCycle]; + + void CaptureCycle(int cycleIndex) + { + DateTime startTime = dataSeries.DataPoints[0].Time; + + for (int i = 0; i < samplesPerCycle; i++) + { + DateTime time = dataSeries.DataPoints[cycleIndex + i].Time; + double value = dataSeries.DataPoints[cycleIndex + i].Value; + tValues[i] = time.Subtract(startTime).TotalSeconds; + yValues[i] = value; + } + } + + // Obtain a list of time gaps in the data series + List gapIndexes = Enumerable.Range(0, dataSeries.DataPoints.Count - 1) + .Where(index => + { + DataPoint p1 = dataSeries[index]; + DataPoint p2 = dataSeries[index + 1]; + double cycleDiff = (p2.Time - p1.Time).TotalSeconds * frequency; + + // Detect gaps larger than a quarter cycle. + // Tolerance of 0.000062 calculated + // assuming 3.999 samples per cycle + return (cycleDiff > 0.250062); + }) + .ToList(); + + double sum = 0; + + if (dataSeries.DataPoints.Count >= samplesPerCycle) + { + CaptureCycle(0); + sum = yValues.Sum(y => y * y); + + DateTime cycleTime = dataSeries.DataPoints[0].Time; + SineWave sineFit = WaveFit.SineFit(yValues, tValues, frequency); + double phase = sineFit.Phase; + + double ComputeSineError() => tValues + .Select(sineFit.CalculateY) + .Zip(yValues, (estimate, value) => Math.Abs(estimate - value)) + .Sum(); + + double sineError = ComputeSineError(); + double previousSineError = sineError; + + rmsSeries.DataPoints.Add(new DataPoint() + { + Time = cycleTime, + Value = Math.Sqrt(sum / samplesPerCycle) + }); + + phaseSeries.DataPoints.Add(new DataPoint() + { + Time = cycleTime, + Value = phase + }); + + peakSeries.DataPoints.Add(new DataPoint() + { + Time = cycleTime, + Value = sineFit.Amplitude + }); + + errorSeries.DataPoints.Add(new DataPoint() + { + Time = cycleTime, + Value = sineError + }); + + // Reduce RMS to max 2 pt per cycle to get half cycle RMS + int step = 1; + if (compress) + step = (int)Math.Floor(samplesPerCycle / 2.0D); + if (step == 0) + step = 1; + + for (int cycleIndex = step; cycleIndex < dataSeries.DataPoints.Count - samplesPerCycle + 1; cycleIndex += step) + { + for (int j = 0; j < step; j++) + { + int oldIndex = cycleIndex - step + j; + int newIndex = oldIndex + samplesPerCycle; + double oldValue = dataSeries.DataPoints[oldIndex].Value; + double newValue = dataSeries.DataPoints[newIndex].Value; + sum += newValue * newValue - oldValue * oldValue; + } + + // If the cycle following i contains a data gap, do not calculate cycle data + if (gapIndexes.Any(index => cycleIndex <= index && (cycleIndex + samplesPerCycle - 1) > index)) + continue; + + phase += 2 * Math.PI * frequency * (dataSeries.DataPoints[cycleIndex].Time - cycleTime).TotalSeconds; + + // Use the time of the first data point in the cycle as the time of the cycle + cycleTime = dataSeries.DataPoints[cycleIndex].Time; + + CaptureCycle(cycleIndex); + + if (compress) + sineError = ComputeSineError(); + + if (!compress || Math.Abs(previousSineError - sineError) > sineError * 0.0001) + { + sineFit = WaveFit.SineFit(yValues, tValues, frequency); + phase = sineFit.Phase; + sineError = ComputeSineError(); + } + + previousSineError = sineError; + + rmsSeries.DataPoints.Add(new DataPoint() + { + Time = cycleTime, + Value = Math.Sqrt(sum / samplesPerCycle) + }); + + phaseSeries.DataPoints.Add(new DataPoint() + { + Time = cycleTime, + Value = phase + }); + + peakSeries.DataPoints.Add(new DataPoint() + { + Time = cycleTime, + Value = sineFit.Amplitude + }); + + errorSeries.DataPoints.Add(new DataPoint() + { + Time = cycleTime, + Value = sineError + }); + } + } + + // Add a series to the data group for each series of cycle data + DataGroup dataGroup = new DataGroup(); + dataGroup.Add(rmsSeries); + dataGroup.Add(phaseSeries); + dataGroup.Add(peakSeries); + dataGroup.Add(errorSeries); + + return new CycleDataGroup(dataGroup, dataSeries.SeriesInfo.Channel.Asset); + } + + public static DataSeries ToRMS(DataSeries dataSeries, double frequency, bool compress = false) + { + DataSeries rmsSeries = new DataSeries(); + + int samplesPerCycle; + double[] yValues; + double[] tValues; + double sum; + + DateTime cycleTime; + + if ((object)dataSeries == null) + return null; + + // Set series info to the source series info + rmsSeries.SeriesInfo = dataSeries.SeriesInfo; + + + // Get samples per cycle of the data series based on the given frequency + samplesPerCycle = Transform.CalculateSamplesPerCycle(dataSeries, frequency); + + //preinitialize size of SeriesInfo + int ncycleData = dataSeries.DataPoints.Count - samplesPerCycle; + rmsSeries.DataPoints = new List(ncycleData); + + + + // Initialize arrays of y-values and t-values for calculating cycle data as necessary + yValues = new double[samplesPerCycle]; + tValues = new double[samplesPerCycle]; + + // Obtain a list of time gaps in the data series + List gapIndexes = Enumerable.Range(0, dataSeries.DataPoints.Count - 1) + .Where(index => + { + DataPoint p1 = dataSeries[index]; + DataPoint p2 = dataSeries[index + 1]; + double cycleDiff = (p2.Time - p1.Time).TotalSeconds * frequency; + + // Detect gaps larger than a quarter cycle. + // Tolerance of 0.000062 calculated + // assuming 3.999 samples per cycle + return (cycleDiff > 0.250062); + }) + .ToList(); + + sum = 0; + + if (dataSeries.DataPoints.Count > samplesPerCycle) + { + sum = dataSeries.DataPoints.Take(samplesPerCycle).Sum(pt => pt.Value * pt.Value); + + rmsSeries.DataPoints.Add(new DataPoint() + { + Time = dataSeries.DataPoints[0].Time, + Value = Math.Sqrt(sum / samplesPerCycle) + }); + + cycleTime = dataSeries.DataPoints[0].Time; + + // Reduce RMS to max 2 pt per cycle to get half cycle RMS + int step = 1; + if (compress) + step = (int)Math.Floor(samplesPerCycle / 2.0D); + if (step == 0) + step = 1; + + for (int i = step; i < dataSeries.DataPoints.Count - samplesPerCycle; i = i + step) + { + + for (int j = 0; j < step; j++) + { + sum = sum - dataSeries.DataPoints[i - step + j].Value * dataSeries.DataPoints[i - step + j].Value; + sum = sum + dataSeries.DataPoints[i - step + j + samplesPerCycle].Value * dataSeries.DataPoints[i - step + j + samplesPerCycle].Value; + } + + // If the cycle following i contains a data gap, do not calculate cycle data + if (gapIndexes.Any(index => i <= index && (i + samplesPerCycle - 1) > index)) + continue; + + // Use the time of the first data point in the cycle as the time of the cycle + cycleTime = dataSeries.DataPoints[i].Time; + + rmsSeries.DataPoints.Add(new DataPoint() + { + Time = cycleTime, + Value = Math.Sqrt(sum / samplesPerCycle) + }); + + } + } + + return rmsSeries; + } + + public static List ToValues(DataSeries series) + { + return series.DataPoints + .Select(dataPoint => dataPoint.Value) + .ToList(); + } + + public static int CalculateSamplesPerCycle(DataSeries dataSeries, double frequency) + { + return CalculateSamplesPerCycle(dataSeries.SampleRate, frequency); + } + + public static int CalculateSamplesPerCycle(double samplesPerSecond, double frequency) + { + int[] commonSampleRates = + { + 4, 8, 16, 32, + 80, 96, 100, 200, + 64, 128, 256, 512, 1024 + }; + + int calculatedRate = (int)Math.Round(samplesPerSecond / frequency); + int nearestCommonRate = commonSampleRates.MinBy(rate => Math.Abs(calculatedRate - rate)); + int diff = Math.Abs(calculatedRate - nearestCommonRate); + return (diff < nearestCommonRate * 0.1D) ? nearestCommonRate : calculatedRate; + } + } +} diff --git a/src/Libraries/FaultData/DataAnalysis/VICycleDataGroup.cs b/src/Libraries/FaultData/DataAnalysis/VICycleDataGroup.cs new file mode 100644 index 00000000..d913f052 --- /dev/null +++ b/src/Libraries/FaultData/DataAnalysis/VICycleDataGroup.cs @@ -0,0 +1,476 @@ +//****************************************************************************************************** +// VICycleDataGroup.cs - Gbtc +// +// Copyright © 2014, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2014 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using FaultAlgorithms; +using openXDA.Model; + +namespace FaultData.DataAnalysis +{ + public class VICycleDataGroup + { + #region [ Members ] + + // Fields + private List m_vIndices; + private Asset m_asset; + + private int m_iaIndex; + private int m_ibIndex; + private int m_icIndex; + private int m_irIndex; + + private List m_cycleDataGroups; + + private class VIndices + { + public int Va; + public int Vb; + public int Vc; + + public int Vab; + public int Vbc; + public int Vca; + + public int distance; + public VIndices() + { + Va = -1; + Vb = -1; + Vc = -1; + Vab = -1; + Vbc = -1; + Vca = -1; + + distance = -1; + } + + public int DefinedNeutralVoltages + { + get + { + return ((Va > -1) ? 1 : 0) + ((Vb > -1) ? 1 : 0) + ((Vc > -1) ? 1 : 0); + } + } + + public int DefinedLineVoltages + { + get + { + return ((Vab > -1) ? 1 : 0) + ((Vbc > -1) ? 1 : 0) + ((Vca > -1) ? 1 : 0); + } + } + + public bool allVoltagesDefined + { + get + { + return ((Vab > -1) && (Vbc > -1) && (Vca > -1) && + (Va > -1) && (Vb > -1) && (Vc > -1)); + } + } + + } + + + public double VBase => m_asset.VoltageKV; + + #endregion + + #region [ Constructors ] + + public VICycleDataGroup(DataGroup dataGroup) + { + m_vIndices = new List(); + m_asset = dataGroup.Asset; + + m_cycleDataGroups = dataGroup.DataSeries + .Select((dataSeries, index) => new { DataSeries = dataSeries, Index = index }) + .GroupBy(obj => obj.Index / 4) + .Where(grouping => grouping.Count() >= 4) + .Select(grouping => grouping.Select(obj => obj.DataSeries)) + .Select(grouping => new CycleDataGroup(new DataGroup(grouping, dataGroup.Asset), dataGroup.Asset)) + .ToList(); + + MapIndexes(); + } + + public VICycleDataGroup(List cycleDataGroups, Asset asset) + { + m_vIndices = new List(); + m_cycleDataGroups = new List(cycleDataGroups); + m_asset = asset; + MapIndexes(); + } + + #endregion + + #region [ Properties ] + + public CycleDataGroup VA + { + get + { + return (m_vIndices.Count > 0 && m_vIndices[0].Va >= 0) ? m_cycleDataGroups[m_vIndices[0].Va] : null; + } + } + + public CycleDataGroup VB + { + get + { + return (m_vIndices.Count > 0 && m_vIndices[0].Vb >= 0) ? m_cycleDataGroups[m_vIndices[0].Vb] : null; + } + } + + public CycleDataGroup VC + { + get + { + return (m_vIndices.Count > 0 && m_vIndices[0].Vc >= 0) ? m_cycleDataGroups[m_vIndices[0].Vc] : null; + } + } + + public CycleDataGroup VAB + { + get + { + return (m_vIndices.Count > 0 && m_vIndices[0].Vab >= 0) ? m_cycleDataGroups[m_vIndices[0].Vab] : null; + } + } + + public CycleDataGroup VBC + { + get + { + return (m_vIndices.Count > 0 && m_vIndices[0].Vbc >= 0) ? m_cycleDataGroups[m_vIndices[0].Vbc] : null; + } + } + + public CycleDataGroup VCA + { + get + { + return (m_vIndices.Count > 0 && m_vIndices[0].Vca >= 0) ? m_cycleDataGroups[m_vIndices[0].Vca] : null; + } + } + + public CycleDataGroup IA + { + get + { + return (m_iaIndex >= 0) ? m_cycleDataGroups[m_iaIndex] : null; + } + } + + public CycleDataGroup IB + { + get + { + return (m_ibIndex >= 0) ? m_cycleDataGroups[m_ibIndex] : null; + } + } + + public CycleDataGroup IC + { + get + { + return (m_icIndex >= 0) ? m_cycleDataGroups[m_icIndex] : null; + } + } + + public CycleDataGroup IR + { + get + { + return (m_irIndex >= 0) ? m_cycleDataGroups[m_irIndex] : null; + } + } + + public List CycleDataGroups { + get { + return m_cycleDataGroups; + } + } + + #endregion + + #region [ Methods ] + + public DataGroup ToDataGroup() + { + return Transform.Combine(m_cycleDataGroups + .Select(cycleDataGroup => cycleDataGroup.ToDataGroup()) + .ToArray()); + } + + public VICycleDataGroup ToSubSet(int startIndex, int endIndex) + { + return new VICycleDataGroup(m_cycleDataGroups + .Select(cycleDataGroup => cycleDataGroup.ToSubGroup(startIndex, endIndex)) + .ToList(), m_asset); + } + + public VICycleDataGroup ToSubSet(DateTime startTime, DateTime endTime) + { + return new VICycleDataGroup(m_cycleDataGroups + .Select(cycleDataGroup => cycleDataGroup.ToSubGroup(startTime, endTime)) + .ToList(), m_asset); + } + + public void PushDataTo(CycleDataSet cycleDataSet) + { + FaultAlgorithms.CycleData cycleData; + Cycle[] cycles; + CycleDataGroup[] cycleDataGroups; + + cycleDataGroups = new CycleDataGroup[] { VA, VB, VC, IA, IB, IC }; + cycles = new Cycle[cycleDataGroups.Length]; + + for (int i = 0; i < VA.ToDataGroup().Samples; i++) + { + cycleData = new FaultAlgorithms.CycleData(); + + cycles[0] = cycleData.AN.V; + cycles[1] = cycleData.BN.V; + cycles[2] = cycleData.CN.V; + cycles[3] = cycleData.AN.I; + cycles[4] = cycleData.BN.I; + cycles[5] = cycleData.CN.I; + + for (int j = 0; j < cycles.Length; j++) + { + if (cycleDataGroups[j] == null) + continue; + + cycles[j].RMS = cycleDataGroups[j].RMS[i].Value; + cycles[j].Phase = cycleDataGroups[j].Phase[i].Value; + cycles[j].Peak = cycleDataGroups[j].Peak[i].Value; + cycles[j].Error = cycleDataGroups[j].Error[i].Value; + } + + cycleDataSet[i] = cycleData; + } + } + + private void MapIndexes() + { + + m_iaIndex = -1; + m_ibIndex = -1; + m_icIndex = -1; + m_irIndex = -1; + + List vaIndices = new List(); + List vbIndices = new List(); + List vcIndices = new List(); + List vabIndices = new List(); + List vbcIndices = new List(); + List vcaIndices = new List(); + + for (int i = 0; i < m_cycleDataGroups.Count; i++) + { + if (isVoltage("AN", m_cycleDataGroups[i])) + vaIndices.Add(i); + else if (isVoltage("BN", m_cycleDataGroups[i])) + vbIndices.Add(i); + else if (isVoltage("CN", m_cycleDataGroups[i])) + vcIndices.Add(i); + else if (isVoltage("AB", m_cycleDataGroups[i])) + vabIndices.Add(i); + else if (isVoltage("BC", m_cycleDataGroups[i])) + vbcIndices.Add(i); + else if (isVoltage("CA", m_cycleDataGroups[i])) + vcaIndices.Add(i); + + } + + //Walk through all Va and try to get corresponding Vb and Vc... + List ProcessedIndices = new List(); + foreach (int? VaIndex in vaIndices) + { + int assetID = m_cycleDataGroups[(int)VaIndex].Asset.ID; + + int VbIndex = vbIndices.Cast().FirstOrDefault(i => m_cycleDataGroups[(int)i].Asset.ID == assetID && !ProcessedIndices.Contains(i)) ?? -1; + int VcIndex = vcIndices.Cast().FirstOrDefault(i => m_cycleDataGroups[(int)i].Asset.ID == assetID && !ProcessedIndices.Contains(i)) ?? -1; + int VabIndex = vabIndices.Cast().FirstOrDefault(i => m_cycleDataGroups[(int)i].Asset.ID == assetID && !ProcessedIndices.Contains(i)) ?? -1; + int VbcIndex = vbcIndices.Cast().FirstOrDefault(i => m_cycleDataGroups[(int)i].Asset.ID == assetID && !ProcessedIndices.Contains(i)) ?? -1; + int VcaIndex = vcaIndices.Cast().FirstOrDefault(i => m_cycleDataGroups[(int)i].Asset.ID == assetID && !ProcessedIndices.Contains(i)) ?? -1; + + VIndices set = new VIndices(); + ProcessedIndices.Add(VaIndex); + set.Va = (int)VaIndex; + + if (VbIndex > -1) + { + ProcessedIndices.Add(VbIndex); + set.Vb = VbIndex; + } + if (VcIndex > -1) + { + ProcessedIndices.Add(VcIndex); + set.Vc = VcIndex; + } + + if (VabIndex > -1) + { + ProcessedIndices.Add(VabIndex); + set.Vab = VabIndex; + } + if (VbcIndex > -1) + { + ProcessedIndices.Add(VbcIndex); + set.Vbc = VbcIndex; + } + if (VcaIndex > -1) + { + ProcessedIndices.Add(VcaIndex); + set.Vca = VcaIndex; + } + + + if (assetID == m_asset.ID) + { + set.distance = 0; + } + else + { + set.distance = m_asset.DistanceToAsset(assetID); + } + + m_vIndices.Add(set); + } + + // Also walk though all Vab to catch Leftover Cases where Va is not present + foreach (int? VabIndex in vabIndices) + { + int assetID = m_cycleDataGroups[(int)VabIndex].Asset.ID; + + int VaIndex = vaIndices.Cast().FirstOrDefault(i => m_cycleDataGroups[(int)i].Asset.ID == assetID && !ProcessedIndices.Contains(i)) ?? -1; + int VbIndex = vbIndices.Cast().FirstOrDefault(i => m_cycleDataGroups[(int)i].Asset.ID == assetID && !ProcessedIndices.Contains(i)) ?? -1; + int VcIndex = vcIndices.Cast().FirstOrDefault(i => m_cycleDataGroups[(int)i].Asset.ID == assetID && !ProcessedIndices.Contains(i)) ?? -1; + + int VbcIndex = vbcIndices.Cast().FirstOrDefault(i => m_cycleDataGroups[(int)i].Asset.ID == assetID && !ProcessedIndices.Contains(i)) ?? -1; + int VcaIndex = vcaIndices.Cast().FirstOrDefault(i => m_cycleDataGroups[(int)i].Asset.ID == assetID && !ProcessedIndices.Contains(i)) ?? -1; + + VIndices set = new VIndices(); + ProcessedIndices.Add(VabIndex); + set.Vab = (int)VabIndex; + + if (VbIndex > -1) + { + ProcessedIndices.Add(VbIndex); + set.Vb = VbIndex; + } + if (VcIndex > -1) + { + ProcessedIndices.Add(VcIndex); + set.Vc = VcIndex; + } + + if (VaIndex > -1) + { + ProcessedIndices.Add(VaIndex); + set.Va = VaIndex; + } + if (VbcIndex > -1) + { + ProcessedIndices.Add(VbcIndex); + set.Vbc = VbcIndex; + } + if (VcaIndex > -1) + { + ProcessedIndices.Add(VcaIndex); + set.Vca = VcaIndex; + } + + + if (assetID == m_asset.ID) + { + set.distance = 0; + } + else + { + set.distance = m_asset.DistanceToAsset(assetID); + } + + m_vIndices.Add(set); + } + + for (int i = 0; i < m_cycleDataGroups.Count; i++) + { + string measurementType = m_cycleDataGroups[i].RMS.SeriesInfo.Channel.MeasurementType.Name; + string phase = m_cycleDataGroups[i].RMS.SeriesInfo.Channel.Phase.Name; + + + if (measurementType == "Current" && phase == "AN") + m_iaIndex = i; + else if (measurementType == "Current" && phase == "BN") + m_ibIndex = i; + else if (measurementType == "Current" && phase == "CN") + m_icIndex = i; + else if (measurementType == "Current" && phase == "RES") + m_irIndex = i; + } + } + + #endregion + + #region [ Static ] + + // Static Methods + + private static bool isVoltage(string phase, CycleDataGroup dataGroup) + { + + string measurementType = dataGroup.RMS.SeriesInfo.Channel.MeasurementType.Name; + string seriesPhase = dataGroup.RMS.SeriesInfo.Channel.Phase.Name; + + if (measurementType != "Voltage") + return false; + + if (seriesPhase != phase) + return false; + + return true; + + } + + private static bool isCurrent(string phase, CycleDataGroup dataGroup) + { + string measurementType = dataGroup.RMS.SeriesInfo.Channel.MeasurementType.Name; + string seriesPhase = dataGroup.RMS.SeriesInfo.Channel.Phase.Name; + + if (measurementType != "Current") + return false; + + if (seriesPhase != phase) + return false; + + return true; + + } + + #endregion + + } +} diff --git a/src/Libraries/FaultData/DataAnalysis/VIDataGroup.cs b/src/Libraries/FaultData/DataAnalysis/VIDataGroup.cs new file mode 100644 index 00000000..ae088e05 --- /dev/null +++ b/src/Libraries/FaultData/DataAnalysis/VIDataGroup.cs @@ -0,0 +1,521 @@ +//****************************************************************************************************** +// VIDataGroup.cs - Gbtc +// +// Copyright © 2014, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2014 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data; +using openXDA.Model; + +namespace FaultData.DataAnalysis +{ + public class VIDataGroup + { + #region [ Members ] + + // Fields + private List m_vIndices; + + private int m_iaIndex; + private int m_ibIndex; + private int m_icIndex; + private int m_irIndex; + + private DataGroup m_dataGroup; + + private class VIndices + { + public int Va { get; set; } = -1; + public int Vb { get; set; } = -1; + public int Vc { get; set; } = -1; + + public int Vab { get; set; } = -1; + public int Vbc { get; set; } = -1; + public int Vca { get; set; } = -1; + + public int Distance { get; set; } = -1; + + public int DefinedNeutralVoltages => + (Va >= 0 ? 1 : 0) + + (Vb >= 0 ? 1 : 0) + + (Vc >= 0 ? 1 : 0); + + public int DefinedLineVoltages => + (Vab >= 0 ? 1 : 0) + + (Vbc >= 0 ? 1 : 0) + + (Vca >= 0 ? 1 : 0); + + public bool AllVoltagesDefined => + (Va >= 0) && (Vb >= 0) && (Vc >= 0) && + (Vab >= 0) && (Vbc >= 0) && (Vca >= 0); + } + + #endregion + + #region [ Constructors ] + + public VIDataGroup(DataGroup dataGroup) + { + + // Initialize each of + // the indexes to -1 + m_vIndices = new List(); + + m_iaIndex = -1; + m_ibIndex = -1; + m_icIndex = -1; + m_irIndex = -1; + + // Initialize the data group + m_dataGroup = new DataGroup(dataGroup.DataSeries, dataGroup.Asset); + + HashSet connectedAssets = new HashSet(dataGroup.Asset.ConnectedAssets.Select(item => item.ID)); + + var groupings = dataGroup.DataSeries + .Select((DataSeries, Index) => new { DataSeries, Index }) + .Where(item => !(item.DataSeries.SeriesInfo is null)) + .Where(item => item.DataSeries.SeriesInfo.Channel.MeasurementCharacteristic.Name == "Instantaneous") + .Where(item => new[] { "Instantaneous", "Values" }.Contains(item.DataSeries.SeriesInfo.SeriesType.Name)) + .GroupBy(item => item.DataSeries.SeriesInfo.Channel.AssetID) + .OrderBy(grouping => grouping.Key == dataGroup.Asset.ID ? 0 : 1) + .ThenBy(grouping => connectedAssets.Contains(grouping.Key) ? 0 : 1) + .ToList(); + + foreach (var grouping in groupings) + { + VIndices set = new VIndices() { Distance = 0 }; + + int assetID = grouping.Key; + + if (assetID != dataGroup.Asset.ID) + set.Distance = dataGroup.Asset.DistanceToAsset(assetID); + + foreach (var item in grouping) + { + string measurementType = item.DataSeries.SeriesInfo.Channel.MeasurementType.Name; + string phase = item.DataSeries.SeriesInfo.Channel.Phase.Name; + + if (measurementType == "Voltage" && phase == "AN") + set.Va = item.Index; + + if (measurementType == "Voltage" && phase == "BN") + set.Vb = item.Index; + + if (measurementType == "Voltage" && phase == "CN") + set.Vc = item.Index; + + if (measurementType == "Voltage" && phase == "AB") + set.Vab = item.Index; + + if (measurementType == "Voltage" && phase == "BC") + set.Vbc = item.Index; + + if (measurementType == "Voltage" && phase == "CA") + set.Vca = item.Index; + + if (m_iaIndex < 0 && measurementType == "Current" && phase == "AN") + m_iaIndex = item.Index; + + if (m_ibIndex < 0 && measurementType == "Current" && phase == "BN") + m_ibIndex = item.Index; + + if (m_icIndex < 0 && measurementType == "Current" && phase == "CN") + m_icIndex = item.Index; + + if (m_irIndex < 0 && measurementType == "Current" && phase == "RES") + m_irIndex = item.Index; + } + + if (set.DefinedLineVoltages + set.DefinedNeutralVoltages > 0) + m_vIndices.Add(set); + } + + if (m_vIndices.Count() == 0) + m_vIndices.Add(new VIndices()); + + CalculateMissingCurrentChannel(); + CalculateMissingLLVoltageChannels(); + + m_vIndices.Sort((a, b) => + { + if (b.AllVoltagesDefined && !a.AllVoltagesDefined) + return 1; + if (a.AllVoltagesDefined && !b.AllVoltagesDefined) + return -1; + if (!(a.Distance >= 0 && b.Distance >= 0)) + return b.Distance.CompareTo(a.Distance); + return a.Distance.CompareTo(b.Distance); + }); + } + + private VIDataGroup() + { + } + + #endregion + + #region [ Properties ] + + public DataSeries VA => (m_vIndices[0].Va >= 0) + ? m_dataGroup[m_vIndices[0].Va] + : null; + + public DataSeries VB => (m_vIndices[0].Vb >= 0) + ? m_dataGroup[m_vIndices[0].Vb] + : null; + + public DataSeries VC => (m_vIndices[0].Vc >= 0) + ? m_dataGroup[m_vIndices[0].Vc] + : null; + + public DataSeries VAB => (m_vIndices[0].Vab >= 0) + ? m_dataGroup[m_vIndices[0].Vab] + : null; + + public DataSeries VBC => (m_vIndices[0].Vbc >= 0) + ? m_dataGroup[m_vIndices[0].Vbc] + : null; + + public DataSeries VCA => (m_vIndices[0].Vca >= 0) + ? m_dataGroup[m_vIndices[0].Vca] + : null; + + public DataSeries IA => (m_iaIndex >= 0) + ? m_dataGroup[m_iaIndex] + : null; + + public DataSeries IB => (m_ibIndex >= 0) + ? m_dataGroup[m_ibIndex] + : null; + + public DataSeries IC => (m_icIndex >= 0) + ? m_dataGroup[m_icIndex] + : null; + + public DataSeries IR => (m_irIndex >= 0) + ? m_dataGroup[m_irIndex] + : null; + + public int DefinedNeutralVoltages => m_vIndices + .Select(item => item.DefinedNeutralVoltages) + .FirstOrDefault(); + + public int DefinedLineVoltages => m_vIndices + .Select(item => item.DefinedLineVoltages) + .FirstOrDefault(); + + public int DefinedCurrents => + CurrentIndexes.Count(index => index >= 0); + + public int DefinedPhaseCurrents => + PhaseCurrentIndexes.Count(index => index >= 0); + + public bool AllVIChannelsDefined => + m_vIndices[0].AllVoltagesDefined && + CurrentIndexes.All(index => index >= 0); + + private int[] CurrentIndexes => + new int[] { m_iaIndex, m_ibIndex, m_icIndex, m_irIndex }; + + private int[] PhaseCurrentIndexes => + new int[] { m_iaIndex, m_ibIndex, m_icIndex }; + + public Asset Asset => m_dataGroup.Asset; + + public DataSeries[] Data + { + get + { + List result = new List(); + + foreach (VIndices Vindex in m_vIndices) + { + if (Vindex.Va > -1) + result.Add(m_dataGroup[Vindex.Va]); + if (Vindex.Vb > -1) + result.Add(m_dataGroup[Vindex.Vb]); + if (Vindex.Vc > -1) + result.Add(m_dataGroup[Vindex.Vc]); + + if (Vindex.Vab > -1) + result.Add(m_dataGroup[Vindex.Vab]); + if (Vindex.Vbc > -1) + result.Add(m_dataGroup[Vindex.Vbc]); + if (Vindex.Vca > -1) + result.Add(m_dataGroup[Vindex.Vca]); + } + + if (m_iaIndex > -1) + result.Add(m_dataGroup[m_iaIndex]); + if (m_ibIndex > -1) + result.Add(m_dataGroup[m_ibIndex]); + if (m_icIndex > -1) + result.Add(m_dataGroup[m_icIndex]); + if (m_irIndex > -1) + result.Add(m_dataGroup[m_irIndex]); + + return result.ToArray(); + } + } + + #endregion + + #region [ Methods ] + + /// + /// Given three of the four current channels, calculates the + /// missing channel based on the relationship IR = IA + IB + IC. + /// + private void CalculateMissingCurrentChannel() + { + Meter meter; + DataSeries missingSeries; + + // If the data group does not have exactly 3 channels, + // then there is no missing channel or there is not + // enough data to calculate the missing channel + if (DefinedCurrents != 3) + return; + + // Get the meter associated with the channels in this data group + meter = (IA ?? IB).SeriesInfo.Channel.Meter; + + if (m_iaIndex == -1) + { + // Calculate IA = IR - IB - IC + missingSeries = IR.Add(IB.Negate()).Add(IC.Negate()); + missingSeries.SeriesInfo = GetSeriesInfo(meter, IR.SeriesInfo.Channel.Asset, "Current", "AN", m_dataGroup.SamplesPerHour); + missingSeries.Calculated = true; + m_iaIndex = m_dataGroup.DataSeries.Count; + m_dataGroup.Add(missingSeries); + } + else if (m_ibIndex == -1) + { + // Calculate IB = IR - IA - IC + missingSeries = IR.Add(IA.Negate()).Add(IC.Negate()); + missingSeries.SeriesInfo = GetSeriesInfo(meter, IR.SeriesInfo.Channel.Asset, "Current", "BN", m_dataGroup.SamplesPerHour); + missingSeries.Calculated = true; + m_ibIndex = m_dataGroup.DataSeries.Count; + m_dataGroup.Add(missingSeries); + } + else if (m_icIndex == -1) + { + // Calculate IC = IR - IA - IB + missingSeries = IR.Add(IA.Negate()).Add(IB.Negate()); + missingSeries.SeriesInfo = GetSeriesInfo(meter, IR.SeriesInfo.Channel.Asset, "Current", "CN", m_dataGroup.SamplesPerHour); + missingSeries.Calculated = true; + m_icIndex = m_dataGroup.DataSeries.Count; + m_dataGroup.Add(missingSeries); + } + else + { + // Calculate IR = IA + IB + IC + missingSeries = IA.Add(IB).Add(IC); + missingSeries.SeriesInfo = GetSeriesInfo(meter, IA.SeriesInfo.Channel.Asset, "Current", "RES", m_dataGroup.SamplesPerHour); + missingSeries.Calculated = true; + m_irIndex = m_dataGroup.DataSeries.Count; + m_dataGroup.Add(missingSeries); + } + } + + private void CalculateMissingLLVoltageChannels() + { + Meter meter; + DataSeries missingSeries; + + //Do this for every Voltage set + for (int i = 0; i < m_vIndices.Count(); i++) + { + // If all line voltages are already present or there are not + // at least 2 lines we will not perform line to line calculations + if (m_vIndices[i].DefinedLineVoltages == 3 || m_vIndices[i].DefinedNeutralVoltages < 2) + continue; + + // Get the meter associated with the channels in this data group + DataSeries VA = null; + DataSeries VB = null; + DataSeries VC = null; + + if (m_vIndices[i].Va > -1) + VA = m_dataGroup[m_vIndices[i].Va]; + if (m_vIndices[i].Vb > -1) + VB = m_dataGroup[m_vIndices[i].Vb]; + if (m_vIndices[i].Vc > -1) + VC = m_dataGroup[m_vIndices[i].Vc]; + + meter = (VA ?? VB ?? VC).SeriesInfo.Channel.Meter; + + if (m_vIndices[i].Vab == -1 && !(VA is null) && !(VB is null)) + { + // Calculate VAB = VA - VB + missingSeries = VA.Add(VB.Negate()); + missingSeries.SeriesInfo = GetSeriesInfo(meter, VA.SeriesInfo.Channel.Asset, "Voltage", "AB", m_dataGroup.SamplesPerHour); + missingSeries.Calculated = true; + m_vIndices[i].Vab = m_dataGroup.DataSeries.Count; + m_dataGroup.Add(missingSeries); + } + + if (m_vIndices[i].Vbc == -1 && !(VB is null) && !(VC is null)) + { + // Calculate VBC = VB - VC + missingSeries = VB.Add(VC.Negate()); + missingSeries.SeriesInfo = GetSeriesInfo(meter, VB.SeriesInfo.Channel.Asset, "Voltage", "BC", m_dataGroup.SamplesPerHour); + missingSeries.Calculated = true; + m_vIndices[i].Vbc = m_dataGroup.DataSeries.Count; + m_dataGroup.Add(missingSeries); + } + + if (m_vIndices[i].Vca == -1 && !(VC is null) && !(VA is null)) + { + // Calculate VCA = VC - VA + missingSeries = VC.Add(VA.Negate()); + missingSeries.SeriesInfo = GetSeriesInfo(meter, VC.SeriesInfo.Channel.Asset, "Voltage", "CA", m_dataGroup.SamplesPerHour); + missingSeries.Calculated = true; + m_vIndices[i].Vca = m_dataGroup.DataSeries.Count; + m_dataGroup.Add(missingSeries); + } + } + } + + public DataGroup ToDataGroup() + { + return new DataGroup(m_dataGroup.DataSeries, m_dataGroup.Asset); + } + + public VIDataGroup ToSubGroup(int startIndex, int endIndex) + { + VIDataGroup subGroup = new VIDataGroup(); + + subGroup.m_vIndices = m_vIndices; + subGroup.m_iaIndex = m_iaIndex; + subGroup.m_ibIndex = m_ibIndex; + subGroup.m_icIndex = m_icIndex; + subGroup.m_irIndex = m_irIndex; + + subGroup.m_dataGroup = m_dataGroup.ToSubGroup(startIndex, endIndex); + + return subGroup; + } + + public VIDataGroup ToSubGroup(DateTime startTime, DateTime endTime) + { + VIDataGroup subGroup = new VIDataGroup(); + + subGroup.m_vIndices = m_vIndices; + subGroup.m_iaIndex = m_iaIndex; + subGroup.m_ibIndex = m_ibIndex; + subGroup.m_icIndex = m_icIndex; + subGroup.m_irIndex = m_irIndex; + + subGroup.m_dataGroup = m_dataGroup.ToSubGroup(startTime, endTime); + + return subGroup; + } + + #endregion + + #region [ Static ] + + // Static Methods + private static Series GetSeriesInfo(Meter meter, Asset asset, string measurementTypeName, string phaseName, double samplesPerHour) + { + string measurementCharacteristicName = "Instantaneous"; + string seriesTypeName = "Values"; + + char typeDesignation = (measurementTypeName == "Current") ? 'I' : measurementTypeName[0]; + string phaseDesignation = (phaseName == "RES") ? "R" : phaseName.TrimEnd('N'); + string channelName = string.Concat(typeDesignation, phaseDesignation); + + ChannelKey channelKey = new ChannelKey(asset.ID, 0, channelName, measurementTypeName, measurementCharacteristicName, phaseName); + SeriesKey seriesKey = new SeriesKey(channelKey, seriesTypeName); + + Channel dbChannel = (meter.ConnectionFactory is null) + ? meter.Channels.FirstOrDefault(channel => channelKey.Equals(new ChannelKey(channel))) + : FastSearch(meter, channelKey); + + Series dbSeries = dbChannel?.Series + .FirstOrDefault(series => seriesKey.Equals(new SeriesKey(series))); + + if (dbSeries is null) + { + if (dbChannel is null) + { + MeasurementType measurementType = new MeasurementType() { Name = measurementTypeName }; + MeasurementCharacteristic measurementCharacteristic = new MeasurementCharacteristic() { Name = measurementCharacteristicName }; + Phase phase = new Phase() { Name = phaseName }; + + dbChannel = new Channel() + { + MeterID = meter.ID, + AssetID = asset.ID, + MeasurementTypeID = measurementType.ID, + MeasurementCharacteristicID = measurementCharacteristic.ID, + PhaseID = phase.ID, + Name = channelKey.Name, + SamplesPerHour = samplesPerHour, + Description = string.Concat(measurementCharacteristicName, " ", measurementTypeName, " ", phaseName), + Enabled = true, + + Meter = meter, + Asset = asset, + MeasurementType = measurementType, + MeasurementCharacteristic = measurementCharacteristic, + Phase = phase, + Series = new List() + }; + + meter.Channels.Add(dbChannel); + } + + SeriesType seriesType = new SeriesType() { Name = seriesTypeName }; + + dbSeries = new Series() + { + ChannelID = dbChannel.ID, + SeriesTypeID = seriesType.ID, + SourceIndexes = string.Empty, + + Channel = dbChannel, + SeriesType = seriesType + }; + + dbChannel.Series.Add(dbSeries); + } + + return dbSeries; + } + + private static Channel FastSearch(Meter meter, ChannelKey channelKey) + { + using (AdoDataConnection connection = meter.ConnectionFactory()) + { + Channel search = channelKey.Find(connection, meter.ID); + + if (search is null) + return null; + + return meter.Channels + .FirstOrDefault(channel => channel.ID == search.ID); + } + } + + #endregion + } +} diff --git a/src/Libraries/FaultData/FaultData.csproj b/src/Libraries/FaultData/FaultData.csproj new file mode 100644 index 00000000..f4297a8d --- /dev/null +++ b/src/Libraries/FaultData/FaultData.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + diff --git a/src/Libraries/PQDS/DataSeries.cs b/src/Libraries/PQDS/DataSeries.cs new file mode 100644 index 00000000..5767e178 --- /dev/null +++ b/src/Libraries/PQDS/DataSeries.cs @@ -0,0 +1,94 @@ +//****************************************************************************************************** +// DataSeries.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.Linq; + +namespace PQDS +{ + /// + /// Represents a channel in a PQDS File. + /// + public class DataSeries + { + #region[Properties] + + private List m_series; + private string m_label; + + /// + /// A collection of DataPoints. + /// + public List Series + { + get { return m_series; } + set { m_series = value; } + } + + /// + /// Label of the + /// + public string Label { get { return m_label; } } + + /// + /// length in number of points + /// + public int Length => m_series.Count(); + + #endregion[Properties] + + /// + /// Creates a new . + /// + /// Label of the DataSeries + public DataSeries(string label) + { + m_label = label; + m_series = new List(); + + } + #region[methods] + + #endregion[methods] + } + + /// + /// Represents a single Point in the . + /// + public class DataPoint + { + /// + /// Timestamp of the point. + /// + public DateTime Time; + + /// + /// Value of the point. + /// + public double Value; + } + + +} diff --git a/src/Libraries/PQDS/MetaDataTag.cs b/src/Libraries/PQDS/MetaDataTag.cs new file mode 100644 index 00000000..d08fc836 --- /dev/null +++ b/src/Libraries/PQDS/MetaDataTag.cs @@ -0,0 +1,421 @@ +//****************************************************************************************************** +// MetaDataTag.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; + +namespace PQDS +{ + /// + /// PQDS metadata tag Datatypes according to PQDS spec. + /// + public enum PQDSMetaDataType + { + /// + /// An integer representing a single value selected + /// from among a custom, finite set of possibilities + /// + Enumeration = 0, + + /// + /// A number + /// + Numeric = 1, + + /// + /// Text consisting only of alphabetical characters and digits + /// + AlphaNumeric = 2, + + /// + /// Freeform text + /// + Text = 3, + + /// + /// A Boolean value (true/false) + /// + Binary = 4 + + } + + /// + /// Abstract Class of MetaData Tags for a . + /// + public abstract class MetaDataTag + { + #region[Properties] + + /// + /// The key that identifies the metadata tag. + /// + protected string m_key; + + /// + /// The unit of measurement. + /// + protected string m_unit; + + /// + /// The data type the parser expects to encounter for the value of the metdata. + /// + protected PQDSMetaDataType m_expectedDataType; + + /// + /// Additional notes about the metadata field. + /// + protected string m_note; + + #endregion[Properties] + + #region[Methods] + + /// + /// the Metadata Tag key. + /// + public String Key { get { return (this.m_key); } } + + /// + /// Converst the Metadata tag into a line of a PQDS file + /// + /// The metadataTag as a String + public abstract String Write(); + + /// + /// Returns the PQDS datatype + /// + /// The PQDS Datatype + public abstract PQDSMetaDataType Type(); + + #endregion[Methods] + } + + /// + /// Class of MetaData Tags for a . + /// + public class MetaDataTag : MetaDataTag + { + #region[Properties] + + private DataType m_value; + + /// + /// Value of the MetadataTag. + /// + public DataType Value { get { return m_value; } } + + #endregion[Properties] + + #region[Constructor] + + /// + /// Creates a . + /// + /// key of the MetadataTag + /// Value of the MetadataTag + public MetaDataTag(String key, DataType value) + { + this.m_value = value; + + this.m_key = key; + if (!keyToDataTypeLookup.TryGetValue(key, out this.m_expectedDataType)) + this.m_expectedDataType = PQDSMetaDataType.Text; + + if (!keyToUnitLookup.TryGetValue(key, out this.m_unit)) + this.m_unit = null; + + if (!keyToNoteLookup.TryGetValue(key, out this.m_note)) + this.m_note = null; + + //Check to ensure a string does not end up being a number etc... + if (this.m_expectedDataType == PQDSMetaDataType.AlphaNumeric) + { + if (!((value is string) | (value is Guid))) + { throw new InvalidCastException("Can not cast object to Alphanumeric Type"); } + } + else if (this.m_expectedDataType == PQDSMetaDataType.Numeric) + { + if (!((value is int) | (value is double))) + { throw new InvalidCastException("Can not cast object to Numeric Type"); } + } + else if (this.m_expectedDataType == PQDSMetaDataType.Enumeration) + { + if (!((value is int))) + { throw new InvalidCastException("Can not cast object to Numeric Type"); } + } + else if (this.m_expectedDataType == PQDSMetaDataType.Binary) + { + if (!((value is int) | (value is Boolean))) + { throw new InvalidCastException("Can not cast object to Numeric Type"); } + } + + } + + /// + /// Creates a custom . + /// + /// key of the MetadataTag + /// Value of the MetadataTag + /// The of the metadata tag + /// The unit of the metadata tag + /// a describtion of the metadata tag + public MetaDataTag(String key, DataType value, PQDSMetaDataType valueType, String unit, String description) + { + this.m_value = value; + + this.m_key = key; + this.m_expectedDataType = valueType; + + if (unit.Trim('"') == "") { this.m_unit = null; } + else { this.m_unit = unit.Trim('"'); } + + if (description.Trim('"') == "") { this.m_note = null; } + else { this.m_note = description.Trim('"'); } + + } + + #endregion[Constructor] + + #region[Methods] + + /// + /// Converst the Metadata tag into a line of a PQDS file + /// + /// The metadataTag as a String + public override string Write() + { + string result = String.Format("{0},\"{1}\",{2},{3},\"{4}\"", + this.m_key, this.m_value, this.m_unit, DataTypeToCSV(this.m_expectedDataType), this.m_note); + + return result; + } + + /// + /// Returns the PQDS datatype + /// + /// The PQDS Datatype + public override PQDSMetaDataType Type() + { + return this.m_expectedDataType; + } + + #endregion[Methods] + + #region[Statics] + + private static readonly Dictionary keyToDataTypeLookup = new Dictionary() + { + {"DeviceName", PQDSMetaDataType.Text }, + {"DeviceAlias", PQDSMetaDataType.Text }, + {"DeviceLocation", PQDSMetaDataType.Text }, + {"DeviceLocationAlias", PQDSMetaDataType.Text }, + {"DeviceLatitude", PQDSMetaDataType.Text }, + {"DeviceLongitude", PQDSMetaDataType.Text }, + {"Accountname", PQDSMetaDataType.Text }, + {"AccountNameAlias", PQDSMetaDataType.Text }, + {"DeviceDistanceToXFMR", PQDSMetaDataType.Numeric }, + {"DeviceConnectionTypeCode", PQDSMetaDataType.Enumeration }, + {"DeviceOwner", PQDSMetaDataType.Text }, + {"NominalVoltage-LG", PQDSMetaDataType.Numeric }, + {"NominalFrequency", PQDSMetaDataType.Numeric }, + {"UpstreamXFMR-kVA", PQDSMetaDataType.Numeric }, + {"LineLength", PQDSMetaDataType.Numeric }, + {"AssetName", PQDSMetaDataType.Text }, + {"EventGUID", PQDSMetaDataType.AlphaNumeric }, + {"EventID", PQDSMetaDataType.Text }, + {"EventYear", PQDSMetaDataType.Enumeration }, + {"EventMonth", PQDSMetaDataType.Enumeration }, + {"EventDay", PQDSMetaDataType.Enumeration }, + {"EventHour", PQDSMetaDataType.Enumeration }, + {"EventMinute", PQDSMetaDataType.Enumeration }, + {"EventSecond", PQDSMetaDataType.Enumeration }, + {"EventNanoSecond", PQDSMetaDataType.Numeric }, + {"EventDate", PQDSMetaDataType.Text }, + {"EventTime", PQDSMetaDataType.Text }, + {"EventTypeCode", PQDSMetaDataType.Enumeration }, + {"EventFaultTypeCode", PQDSMetaDataType.Enumeration }, + {"EventPeakCurrent", PQDSMetaDataType.Numeric }, + {"EventPeakVoltage", PQDSMetaDataType.Numeric }, + {"EventMaxVA", PQDSMetaDataType.Numeric }, + {"EventMaxVB", PQDSMetaDataType.Numeric }, + {"EventMaxVC", PQDSMetaDataType.Numeric }, + {"EventMinVA", PQDSMetaDataType.Numeric }, + {"EventMinVB", PQDSMetaDataType.Numeric }, + {"EventMinVC", PQDSMetaDataType.Numeric }, + {"EventMaxIA", PQDSMetaDataType.Numeric }, + {"EventMaxIB", PQDSMetaDataType.Numeric }, + {"EventMaxIC", PQDSMetaDataType.Numeric }, + {"EventPreEventCurrent", PQDSMetaDataType.Numeric }, + {"EventPreEventVoltage", PQDSMetaDataType.Numeric }, + {"EventDuration", PQDSMetaDataType.Numeric }, + {"EventFaultI2T", PQDSMetaDataType.Numeric }, + {"DistanceToFault", PQDSMetaDataType.Numeric }, + {"EventCauseCode", PQDSMetaDataType.Enumeration }, + {"WaveformDataType", PQDSMetaDataType.Enumeration }, + {"WaveFormSensitivityCode", PQDSMetaDataType.Enumeration }, + {"WaveFormSensitivityNote", PQDSMetaDataType.Text }, + {"Utility", PQDSMetaDataType.Text }, + {"ContactEmail", PQDSMetaDataType.Text } + }; + + private static readonly Dictionary keyToUnitLookup = new Dictionary() + { + {"DeviceName", null }, + {"DeviceAlias", null }, + {"DeviceLocation", null }, + {"DeviceLocationAlias", null }, + {"DeviceLatitude", null }, + {"DeviceLongitude", null }, + {"Accountname", null }, + {"AccountNameAlias", null }, + {"DeviceDistanceToXFMR", "feet" }, + {"DeviceConnectionTypeCode", null }, + {"DeviceOwner", null }, + {"NominalVoltage-LG", "Volts" }, + {"NominalFrequency", "Hz" }, + {"UpstreamXFMR-kVA", "kVA" }, + {"LineLength", "miles" }, + {"AssetName", null }, + {"EventGUID", null }, + {"EventID", null }, + {"EventYear", null }, + {"EventMonth", null }, + {"EventDay", null }, + {"EventHour", null }, + {"EventMinute", null }, + {"EventSecond", null }, + {"EventNanoSecond", null }, + {"EventDate", null }, + {"EventTime", null }, + {"EventTypeCode", null }, + {"EventFaultTypeCode", null }, + {"EventPeakCurrent", "Amps" }, + {"EventPeakVoltage", "Volts" }, + {"EventMaxVA", "Volts" }, + {"EventMaxVB", "Volts" }, + {"EventMaxVC", "Volts" }, + {"EventMinVA", "Volts" }, + {"EventMinVB", "Volts" }, + {"EventMinVC", "Volts" }, + {"EventMaxIA", "Amps" }, + {"EventMaxIB", "Amps" }, + {"EventMaxIC", "Amps" }, + {"EventPreEventCurrent", "Amps" }, + {"EventPreEventVoltage", "Volts" }, + {"EventDuration", "ms" }, + {"EventFaultI2T", "A2s" }, + {"DistanceToFault", "miles" }, + {"EventCauseCode", null }, + {"WaveformDataType", null }, + {"WaveFormSensitivityCode", null }, + {"WaveFormSensitivityNote", null }, + {"Utility", null }, + {"ContactEmail", null } + }; + + private static readonly Dictionary keyToNoteLookup = new Dictionary() + { + {"DeviceName", "Meter or measurement device name" }, + {"DeviceAlias", "Alternate meter or measurement device name" }, + {"DeviceLocation", "Meter or measurment device location name" }, + {"DeviceLocationAlias", "Alternate meter or device location name" }, + {"DeviceLatitude", "Latitude" }, + {"DeviceLongitude", "Longtitude" }, + {"Accountname", "Name of customer or account" }, + {"AccountNameAlias", "Alternate name of customer or account" }, + {"DeviceDistanceToXFMR", "Distance to the upstream transformer" }, + {"DeviceConnectionTypeCode", "PQDS code for meter connection type" }, + {"DeviceOwner", "Utility name" }, + {"NominalVoltage-LG", "Nominal Line to Ground Voltage" }, + {"NominalFrequency", "Nominal System frequency" }, + {"UpstreamXFMR-kVA", "Upstream Transformer size" }, + {"LineLength", "Length of the Line" }, + {"AssetName", "Asset name" }, + {"EventGUID", "Globally Unique Event Identifier" }, + {"EventID", "A user defined Event Name" }, + {"EventYear", "Year" }, + {"EventMonth", "Month" }, + {"EventDay", "Day" }, + {"EventHour", "Hour" }, + {"EventMinute", "Minute" }, + {"EventSecond", "Second" }, + {"EventNanoSecond", "Nanosconds" }, + {"EventDate", "Event Date" }, + {"EventTime", "Event Time" }, + {"EventTypeCode", "PQDS Event Type Code" }, + {"EventFaultTypeCode", "PQDS Fault Type Code" }, + {"EventPeakCurrent", "Peak Current"}, + {"EventPeakVoltage", "Peak Voltage" }, + {"EventMaxVA", "RMS Maximum A Phase Voltage" }, + {"EventMaxVB", "RMS Maximum B Phase Voltage" }, + {"EventMaxVC", "RMS Maximum C Phase Voltage" }, + {"EventMinVA", "RMS Minimum A Phase Voltage" }, + {"EventMinVB", "RMS Minimum B Phase Voltage" }, + {"EventMinVC", "RMS Minimum C Phase Voltage" }, + {"EventMaxIA", "RMS Maximum A Phase Current" }, + {"EventMaxIB", "RMS Maximum B Phase Current" }, + {"EventMaxIC", "RMS Maximum C Phase Current" }, + {"EventPreEventCurrent", "Pre Event Current" }, + {"EventPreEventVoltage", "pre Event Voltage" }, + {"EventDuration", "Event Duration" }, + {"EventFaultI2T", "I2(t) during Fault duration" }, + {"DistanceToFault", "Distance to Fault" }, + {"EventCauseCode", "PQDS Event Cause Code" }, + { "WaveformDataType", "PQDS Data Type Code"}, + {"WaveFormSensitivityCode", "PQDS Data Sensitivity Code" }, + {"WaveFormSensitivityNote", "Notes on the PQDS Data Sensitivity Code" }, + {"Utility", "Utility that Generated this Dataset" }, + {"ContactEmail", "Contact for Utility that Created this Dataset" } + }; + + private static string DataTypeToCSV(PQDSMetaDataType dataType) + { + switch (dataType) + { + case (PQDSMetaDataType.Text): + return "T"; + case (PQDSMetaDataType.Numeric): + return "N"; + case (PQDSMetaDataType.Enumeration): + return "E"; + case (PQDSMetaDataType.AlphaNumeric): + return "A"; + case (PQDSMetaDataType.Binary): + return "B"; + default: + return "T"; + } + } + + + #endregion[Statics] + + + + + + + } + + +} diff --git a/src/Libraries/PQDS/PQDS.csproj b/src/Libraries/PQDS/PQDS.csproj new file mode 100644 index 00000000..c29cf134 --- /dev/null +++ b/src/Libraries/PQDS/PQDS.csproj @@ -0,0 +1,23 @@ + + + netstandard2.0 + Library + PQDS + PQDS + Copyright © 2020 + 3.0.5.69 + 3.0.5.69 + + + ..\..\..\Build\Output\Debug\Libraries\ + ..\..\..\Build\Output\Debug\Libraries\PQDS.xml + + + ..\..\..\Build\Output\Release\Libraries\ + ..\..\..\Build\Output\Release\Libraries\PQDS.xml + + + + + + \ No newline at end of file diff --git a/src/Libraries/PQDS/PQDSFile.cs b/src/Libraries/PQDS/PQDSFile.cs new file mode 100644 index 00000000..17f3cc52 --- /dev/null +++ b/src/Libraries/PQDS/PQDSFile.cs @@ -0,0 +1,541 @@ +//****************************************************************************************************** +// 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; + +namespace 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; + try + { + double ticks = Convert.ToDouble(flds[0].Trim()); + TS = this.m_initialTS + new TimeSpan((Int64)(ticks * 100)); + } + 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 }); + } + 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] + + } + + +} diff --git a/src/Libraries/PQDS/Properties/AssemblyInfo.cs b/src/Libraries/PQDS/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..b6c46133 --- /dev/null +++ b/src/Libraries/PQDS/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c6e64ba2-dca7-4a34-973a-2306a1f9effc")] diff --git a/src/Libraries/openXDA.Model/Channels/Channel.cs b/src/Libraries/openXDA.Model/Channels/Channel.cs new file mode 100644 index 00000000..491748e4 --- /dev/null +++ b/src/Libraries/openXDA.Model/Channels/Channel.cs @@ -0,0 +1,580 @@ +//****************************************************************************************************** +// Channel.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Data; +using System.Transactions; +using Gemstone.Data; +using Gemstone.Data.Model; +using Newtonsoft.Json; +using IsolationLevel = System.Transactions.IsolationLevel; + +namespace openXDA.Model +{ + public class ChannelKey : IEquatable + { + #region [ Constructors ] + + public ChannelKey(int assetID, int harmonicGroup, string name, string measurementType, string measurementCharacteristic, string phase) + { + LineID = assetID; + HarmonicGroup = harmonicGroup; + Name = name; + MeasurementType = measurementType; + MeasurementCharacteristic = measurementCharacteristic; + Phase = phase; + } + + public ChannelKey(Channel channel) + : this(channel.AssetID, channel.HarmonicGroup, channel.Name, channel.MeasurementType.Name, channel.MeasurementCharacteristic.Name, channel.Phase.Name) + { + } + + #endregion + + #region [ Properties ] + + public int LineID { get; } + public int HarmonicGroup { get; } + public string Name { get; } + public string MeasurementType { get; } + public string MeasurementCharacteristic { get; } + public string Phase { get; } + + #endregion + + #region [ Methods ] + + public Channel Find(AdoDataConnection connection, int meterID) + { + const string QueryFormat = + "SELECT Channel.* " + + "FROM " + + " Channel JOIN " + + " MeasurementType ON Channel.MeasurementTypeID = MeasurementType.ID JOIN " + + " MeasurementCharacteristic ON Channel.MeasurementCharacteristicID = MeasurementCharacteristic.ID JOIN " + + " Phase ON Channel.PhaseID = Phase.ID " + + "WHERE " + + " Channel.MeterID = {0} AND " + + " Channel.AssetID = {1} AND " + + " Channel.HarmonicGroup = {2} AND " + + " Channel.Name = {3} AND " + + " MeasurementType.Name = {4} AND " + + " MeasurementCharacteristic.Name = {5} AND " + + " Phase.Name = {6}"; + + object[] parameters = + { + meterID, + LineID, + HarmonicGroup, + Name, + MeasurementType, + MeasurementCharacteristic, + Phase + }; + + using (DataTable table = connection.RetrieveData(QueryFormat, parameters)) + { + if (table.Rows.Count == 0) + return null; + + TableOperations channelTable = new TableOperations(connection); + return channelTable.LoadRecord(table.Rows[0]); + } + } + + public override int GetHashCode() + { + StringComparer stringComparer = StringComparer.OrdinalIgnoreCase; + + int hash = 1009; + hash = 9176 * hash + LineID.GetHashCode(); + hash = 9176 * hash + HarmonicGroup.GetHashCode(); + hash = 9176 * hash + stringComparer.GetHashCode(Name); + hash = 9176 * hash + stringComparer.GetHashCode(MeasurementType); + hash = 9176 * hash + stringComparer.GetHashCode(MeasurementCharacteristic); + hash = 9176 * hash + stringComparer.GetHashCode(Phase); + return hash; + } + + public override bool Equals(object obj) + { + return Equals(obj as ChannelKey); + } + + public bool Equals(ChannelKey other) + { + if (other is null) + return false; + + StringComparison stringComparison = StringComparison.OrdinalIgnoreCase; + + return + LineID.Equals(other.LineID) && + HarmonicGroup.Equals(other.HarmonicGroup) && + Name.Equals(other.Name, stringComparison) && + MeasurementType.Equals(other.MeasurementType, stringComparison) && + MeasurementCharacteristic.Equals(other.MeasurementCharacteristic, stringComparison) && + Phase.Equals(other.Phase, stringComparison); + } + + #endregion + } + + [TableName("Channel")] + public class ChannelBase + { + [PrimaryKey(true)] + public int ID { get; set; } + + [ParentKey(typeof(Meter))] + public int MeterID { get; set; } + + public int AssetID { get; set; } + + public int MeasurementTypeID { get; set; } + + public int MeasurementCharacteristicID { get; set; } + + public int PhaseID { get; set; } + + [StringLength(200)] + public string Name { get; set; } + + public double Adder { get; set; } + + [DefaultValue(1.0D)] + public double Multiplier { get; set; } = 1.0D; + + public double SamplesPerHour { get; set; } + + public double? PerUnitValue { get; set; } + + public int HarmonicGroup { get; set; } + + public string Description { get; set; } + + public bool Enabled { get; set; } + + [DefaultValue(false)] + public bool Trend { get; set; } + + [DefaultValue(0)] + public int ConnectionPriority { get; set; } = 0; + } + + public class Channel : ChannelBase + { + #region [ Members ] + + // Fields + private MeasurementType m_measurementType; + private MeasurementCharacteristic m_measurementCharacteristic; + private Phase m_phase; + private Meter m_meter; + private Asset m_asset; + private List m_series; + + #endregion + + #region [ Properties ] + + [JsonIgnore] + [NonRecordField] + public MeasurementType MeasurementType + { + get + { + if (m_measurementType is null) + m_measurementType = LazyContext.GetMeasurementType(MeasurementTypeID); + + if (m_measurementType is null) + m_measurementType = QueryMeasurementType(); + + return m_measurementType; + } + set => m_measurementType = value; + } + + [JsonIgnore] + [NonRecordField] + public MeasurementCharacteristic MeasurementCharacteristic + { + get + { + if (m_measurementCharacteristic is null) + m_measurementCharacteristic = LazyContext.GetMeasurementCharacteristic(MeasurementCharacteristicID); + + if (m_measurementCharacteristic is null) + m_measurementCharacteristic = QueryMeasurementCharacteristic(); + + return m_measurementCharacteristic; + } + set => m_measurementCharacteristic = value; + } + + [JsonIgnore] + [NonRecordField] + public Phase Phase + { + get + { + if (m_phase is null) + m_phase = LazyContext.GetPhase(PhaseID); + + if (m_phase is null) + m_phase = QueryPhase(); + + return m_phase; + } + set => m_phase = value; + } + + [JsonIgnore] + [NonRecordField] + public Meter Meter + { + get + { + if (m_meter is null) + m_meter = LazyContext.GetMeter(MeterID); + + if (m_meter is null) + m_meter = QueryMeter(); + + return m_meter; + } + set => m_meter = value; + } + + [JsonIgnore] + [NonRecordField] + public Asset Asset + { + get + { + if (m_asset is null) + m_asset = LazyContext.GetAsset(AssetID); + + if (m_asset is null) + m_asset = QueryAsset(); + + return m_asset; + } + set => m_asset = value; + } + + [JsonIgnore] + [NonRecordField] + public List Series + { + get => m_series ?? (m_series = QuerySeries()); + set => m_series = value; + } + + [JsonIgnore] + [NonRecordField] + public Func ConnectionFactory + { + get => LazyContext.ConnectionFactory; + set => LazyContext.ConnectionFactory = value; + } + + [JsonIgnore] + [NonRecordField] + internal LazyContext LazyContext { get; set; } = new LazyContext(); + + #endregion + + #region [ Methods ] + + public MeasurementType GetMeasurementType(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations measurementTypeTable = new TableOperations(connection); + return measurementTypeTable.QueryRecordWhere("ID = {0}", MeasurementTypeID); + } + + public MeasurementCharacteristic GetMeasurementCharacteristic(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations measurementCharacteristicTable = new TableOperations(connection); + return measurementCharacteristicTable.QueryRecordWhere("ID = {0}", MeasurementCharacteristicID); + } + + public Phase GetPhase(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations phaseTable = new TableOperations(connection); + return phaseTable.QueryRecordWhere("ID = {0}", PhaseID); + } + + public Meter GetMeter(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations meterTable = new TableOperations(connection); + return meterTable.QueryRecordWhere("ID = {0}", MeterID); + } + + public Asset GetAsset(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations assetTable = new TableOperations(connection); + return assetTable.QueryRecordWhere("ID = {0}", AssetID); + } + + public IEnumerable GetSeries(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations seriesTable = new TableOperations(connection); + return seriesTable.QueryRecordsWhere("ChannelID = {0}", ID); + } + + private MeasurementType QueryMeasurementType() + { + MeasurementType measurementType; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + measurementType = GetMeasurementType(connection); + } + + return LazyContext.GetMeasurementType(measurementType); + } + + private MeasurementCharacteristic QueryMeasurementCharacteristic() + { + MeasurementCharacteristic measurementCharacteristic; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + measurementCharacteristic = GetMeasurementCharacteristic(connection); + } + + return LazyContext.GetMeasurementCharacteristic(measurementCharacteristic); + } + + private Phase QueryPhase() + { + Phase phase; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + phase = GetPhase(connection); + } + + return LazyContext.GetPhase(phase); + } + + private Meter QueryMeter() + { + Meter meter; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + meter = GetMeter(connection); + } + + if ((object)meter != null) + meter.LazyContext = LazyContext; + + return LazyContext.GetMeter(meter); + } + + private Asset QueryAsset() + { + Asset asset; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + asset = GetAsset(connection); + } + + if ((object)asset != null) + asset.LazyContext = LazyContext; + + return LazyContext.GetAsset(asset); + } + + private List QuerySeries() + { + List seriesList; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + seriesList = GetSeries(connection)? + .Select(LazyContext.GetSeries) + .ToList(); + } + + if ((object)seriesList != null) + { + foreach (Series series in seriesList) + { + series.Channel = this; + series.LazyContext = LazyContext; + } + } + + return seriesList; + } + + #endregion + } + + public class ChannelComparer : IEqualityComparer + { + public bool Equals(Channel x, Channel y) + { + if (Object.ReferenceEquals(x, y)) return true; + + //Check whether any of the compared objects is null. + if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null)) + return false; + + //Check whether the channels are equal. + return x.ID == y.ID; + } + + public int GetHashCode(Channel obj) + { + return obj.ID; + } + } + + public class ChannelInfo + { + [PrimaryKey(true)] + public int ChannelID { get; set; } + + public string ChannelName { get; set; } + + public string ChannelDescription { get; set; } + + public string MeasurementType { get; set; } + + public string MeasurementCharacteristic { get; set; } + + public string Phase { get; set; } + + public string SeriesType { get; set; } + + public string Orientation { get; set; } + + public string Phasing { get; set; } + } + + public static partial class TableOperationsExtensions + { + public static DashSettings GetOrAdd(this TableOperations table, string name, string value, bool enabled = true) + { + TransactionScopeOption required = TransactionScopeOption.Required; + + TransactionOptions transactionOptions = new TransactionOptions() + { + IsolationLevel = IsolationLevel.ReadCommitted, + Timeout = TransactionManager.MaximumTimeout + }; + + DashSettings dashSettings; + + using (TransactionScope transactionScope = new TransactionScope(required, transactionOptions)) + { + if (value.Contains(",")) + dashSettings = table.QueryRecordWhere("Name = {0} AND SUBSTRING(Value, 0, CHARINDEX(',', Value)) = {1}", name, value.Split(',').First()); + else + dashSettings = table.QueryRecordWhere("Name = {0} AND Value = {1}", name, value); + + if ((object)dashSettings == null) + { + dashSettings = new DashSettings(); + dashSettings.Name = name; + dashSettings.Value = value; + dashSettings.Enabled = enabled; + + table.AddNewRecord(dashSettings); + + dashSettings.ID = table.Connection.ExecuteScalar("SELECT @@IDENTITY"); + } + + transactionScope.Complete(); + } + + return dashSettings; + } + + public static UserDashSettings GetOrAdd(this TableOperations table, string name, Guid user, string value, bool enabled = true) + { + TransactionScopeOption required = TransactionScopeOption.Required; + + TransactionOptions transactionOptions = new TransactionOptions() + { + IsolationLevel = IsolationLevel.ReadCommitted, + Timeout = TransactionManager.MaximumTimeout + }; + + UserDashSettings dashSettings; + + using (TransactionScope transactionScope = new TransactionScope(required, transactionOptions)) + { + if (value.Contains(",")) + dashSettings = table.QueryRecordWhere("Name = {0} AND SUBSTRING(Value, 0, CHARINDEX(',', Value)) = {1} AND UserAccountID = {2}", name, value.Split(',').First(), user); + else + dashSettings = table.QueryRecordWhere("Name = {0} AND Value = {1} AND UserAccountID = {2}", name, value, user); + + if ((object)dashSettings == null) + { + dashSettings = new UserDashSettings(); + dashSettings.Name = name; + dashSettings.Value = value; + dashSettings.Enabled = enabled; + dashSettings.UserAccountID = user; + + table.AddNewRecord(dashSettings); + + dashSettings.ID = table.Connection.ExecuteScalar("SELECT @@IDENTITY"); + } + + transactionScope.Complete(); + } + + return dashSettings; + } + + } + +} \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/Channels/ChannelData.cs b/src/Libraries/openXDA.Model/Channels/ChannelData.cs new file mode 100644 index 00000000..2ce28f94 --- /dev/null +++ b/src/Libraries/openXDA.Model/Channels/ChannelData.cs @@ -0,0 +1,649 @@ +//****************************************************************************************************** +// ChannelData.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 12/12/2019 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Data; +using Gemstone; +using Gemstone.Data; +using Gemstone.Data.DataExtensions; +using Gemstone.Data.Model; +using Ionic.Zlib; + +namespace openXDA.Model +{ + [TableName("ChannelData")] + public class ChannelData + { + #region [ Members ] + + // Nested Types + private class DigitalSection + { + public DateTime Start { get; set; } + public DateTime End { get; set; } + public int NumPoints { get; set; } + public double Value { get; set; } + public static int Size => 2 * sizeof(long) + sizeof(ushort) + sizeof(int); + + public int CopyBytes(byte[] byteArray, int offset, double compressionScale, double compressionOffset) + { + ushort compressedValue = (ushort)Math.Round((Value - compressionOffset) * compressionScale); + const ushort NaNValue = ushort.MaxValue; + + if (compressedValue == NaNValue) + compressedValue--; + + if (double.IsNaN(Value)) + compressedValue = NaNValue; + + int startOffset = offset; + offset += LittleEndian.CopyBytes(Start.Ticks, byteArray, offset); + offset += LittleEndian.CopyBytes(End.Ticks, byteArray, offset); + offset += LittleEndian.CopyBytes(compressedValue, byteArray, offset); + offset += LittleEndian.CopyBytes(NumPoints, byteArray, offset); + return offset - startOffset; + } + + public static DigitalSection FromBytes(byte[] bytes, int offset, double decompressionOffset, double decompressionScale) + { + DigitalSection section = new DigitalSection(); + + section.Start = new DateTime(LittleEndian.ToInt64(bytes, offset)); + offset += sizeof(long); + + section.End = new DateTime(LittleEndian.ToInt64(bytes, offset)); + offset += sizeof(long); + + ushort compressedValue = LittleEndian.ToUInt16(bytes, offset); + section.Value = decompressionScale * compressedValue + decompressionOffset; + offset += sizeof(ushort); + + section.NumPoints = LittleEndian.ToInt32(bytes, offset); + + return section; + } + } + #endregion + + #region [ Properties ] + + [PrimaryKey(true)] + public int ID { get; set; } + + public int SeriesID { get; set; } + + public int EventID { get; set; } + + public byte[] TimeDomainData { get; set; } + + public int MarkedForDeletion { get; set; } + + #endregion + + #region [ Methods ] + + /// + /// Adjusts the TimeDomain Data by Moving it a certain ammount of Time + /// + /// The number of Ticks the Data is moved. For moving it backwards in Time this needs to be < 0 + public void AdjustData(Ticks ticks) + { + // Initially we assume Data is already migrated... + if (TimeDomainData == null) + return; + + Tuple> decompressed = Decompress(TimeDomainData)[0]; + List data = decompressed.Item2; + + foreach (DataPoint dataPoint in data) + dataPoint.Time = dataPoint.Time.AddTicks(ticks); + + TimeDomainData = ToData(data, decompressed.Item1); + } + + #endregion + + #region [ Static ] + + public static List DataFromEvent(int eventID, Func connectionFactory) + { + using (AdoDataConnection connection = connectionFactory()) + { + TableOperations eventTable = new TableOperations(connection); + Event evt = eventTable.QueryRecordWhere("ID = {0}", eventID); + + TableOperations assetTable = new TableOperations(connection); + Asset asset = assetTable.QueryRecordWhere("ID = {0}", evt.AssetID); + asset.ConnectionFactory = connectionFactory; + + List channels = asset.DirectChannels + .Concat(asset.ConnectedChannels) + .Where(channel => channel.MeterID == evt.MeterID) + .ToList(); + + if (!channels.Any()) + return new List(); + + IEnumerable assetIDs = channels + .Select(channel => channel.AssetID) + .Distinct(); + + foreach (int assetID in assetIDs) + MigrateLegacyBlob(connection, evt.FileGroupID, assetID, evt.StartTime); + + // Optimization to avoid individually querying channels that don't have any data + HashSet channelsWithData = QueryChannelsWithData(connection, evt); + channels.RemoveAll(channel => !channelsWithData.Contains(channel.ID)); + + List eventData = new List(); + + foreach (Channel channel in channels) + { + const string DataQueryFormat = + "SELECT ChannelData.TimeDomainData " + + "FROM " + + " ChannelData JOIN " + + " Series ON ChannelData.SeriesID = Series.ID JOIN " + + " Event ON ChannelData.EventID = Event.ID " + + "WHERE " + + " Event.FileGroupID = {0} AND " + + " Series.ChannelID = {1} AND " + + " Event.StartTime = {2}"; + + object startTime2 = ToDateTime2(connection, evt.StartTime); + byte[] timeDomainData = connection.ExecuteScalar(DataQueryFormat, evt.FileGroupID, channel.ID, startTime2); + + if (timeDomainData is null) + continue; + + eventData.Add(timeDomainData); + } + + return eventData; + } + } + + public static byte[] DataFromEvent(int eventID, int channelID, Func connectionFactory) + { + using (AdoDataConnection connection = connectionFactory()) + { + TableOperations eventTable = new TableOperations(connection); + Event evt = eventTable.QueryRecordWhere("ID = {0}", eventID); + MigrateLegacyBlob(connection, evt); + + const string QueryFormat = + "SELECT ChannelData.TimeDomainData " + + "FROM " + + " ChannelData JOIN " + + " Series ON ChannelData.SeriesID = Series.ID " + + "WHERE " + + " ChannelData.EventID = {0} AND " + + " Series.ChannelID = {1}"; + + return connection.ExecuteScalar(QueryFormat, eventID, channelID); + } + } + + private static void MigrateLegacyBlob(AdoDataConnection connection, int fileGroupID, int assetID, DateTime startTime) + { + const string AssetQueryFilter = "FileGroupID = {0} AND AssetID = {1} AND StartTime = {2}"; + object startTime2 = ToDateTime2(connection, startTime); + + TableOperations eventTable = new TableOperations(connection); + Event evt = eventTable.QueryRecordWhere(AssetQueryFilter, fileGroupID, assetID, startTime2); + MigrateLegacyBlob(connection, evt); + } + + private static void MigrateLegacyBlob(AdoDataConnection connection, Event evt) + { + if (evt is null || evt.EventDataID is null) + return; + + int eventDataID = evt.EventDataID.GetValueOrDefault(); + byte[] timeDomainData = connection.ExecuteScalar("SELECT TimeDomainData FROM EventData WHERE ID = {0}", eventDataID); + List>> decompressedData = Decompress(timeDomainData); + + TableOperations channelDataTable = new TableOperations(connection); + + foreach (Tuple> tuple in decompressedData) + { + int seriesID = tuple.Item1; + List data = tuple.Item2; + + ChannelData channelData = new ChannelData(); + channelData.SeriesID = seriesID; + channelData.EventID = evt.ID; + channelData.TimeDomainData = ToData(data, seriesID); + channelDataTable.AddNewRecord(channelData); + } + + connection.ExecuteNonQuery("UPDATE Event SET EventDataID = NULL WHERE ID = {0}", evt.ID); + connection.ExecuteNonQuery("DELETE FROM EventData WHERE ID = {0}", eventDataID); + } + + /// + /// Turns a list of DataPoints into a blob to be saved in the database. + /// + /// The data as a + /// The SeriesID to be encoded into the blob + /// The byte array to be saved as a blob in the database. + public static byte[] ToData(List data, int seriesID) + { + // We can use Digital compression if the data changes no more than 10% of the time. + bool useDigitalCompression = data + .Skip(1) + .Zip(data, (p2, p1) => new { p1, p2 }) + .Where(obj => obj.p1.Value != obj.p2.Value) + .Select((_, index) => index + 1) + .All(nChanges => nChanges <= 0.1 * data.Count); + + if (useDigitalCompression) + return ToDigitalData(data, seriesID); + + var timeSeries = data.Select(dataPoint => new { Time = dataPoint.Time.Ticks, Compressed = false }).ToList(); + + for (int i = 1; i < timeSeries.Count; i++) + { + long previousTimestamp = data[i - 1].Time.Ticks; + long timestamp = timeSeries[i].Time; + long diff = timestamp - previousTimestamp; + + if (diff >= 0 && diff <= ushort.MaxValue) + timeSeries[i] = new { Time = diff, Compressed = true }; + } + + int timeSeriesByteLength = timeSeries.Sum(obj => obj.Compressed ? sizeof(ushort) : sizeof(int) + sizeof(long)); + int dataSeriesByteLength = sizeof(int) + (2 * sizeof(double)) + (data.Count * sizeof(ushort)); + int totalByteLength = sizeof(int) + timeSeriesByteLength + dataSeriesByteLength; + + byte[] result = new byte[totalByteLength]; + int offset = 0; + + offset += LittleEndian.CopyBytes(data.Count, result, offset); + + List uncompressedIndexes = timeSeries + .Select((obj, Index) => new { obj.Compressed, Index }) + .Where(obj => !obj.Compressed) + .Select(obj => obj.Index) + .ToList(); + + for (int i = 0; i < uncompressedIndexes.Count; i++) + { + int index = uncompressedIndexes[i]; + int nextIndex = (i + 1 < uncompressedIndexes.Count) ? uncompressedIndexes[i + 1] : timeSeries.Count; + + offset += LittleEndian.CopyBytes(nextIndex - index, result, offset); + offset += LittleEndian.CopyBytes(timeSeries[index].Time, result, offset); + + for (int j = index + 1; j < nextIndex; j++) + offset += LittleEndian.CopyBytes((ushort)timeSeries[j].Time, result, offset); + } + + const ushort NaNValue = ushort.MaxValue; + const ushort MaxCompressedValue = ushort.MaxValue - 1; + double range = data.Select(item => item.Value).Max() - data.Select(item => item.Value).Min(); + double decompressionOffset = data.Select(item => item.Value).Min(); + double decompressionScale = range / MaxCompressedValue; + double compressionScale = (decompressionScale != 0.0D) ? 1.0D / decompressionScale : 0.0D; + + offset += LittleEndian.CopyBytes(seriesID, result, offset); + offset += LittleEndian.CopyBytes(decompressionOffset, result, offset); + offset += LittleEndian.CopyBytes(decompressionScale, result, offset); + + foreach (DataPoint dataPoint in data) + { + ushort compressedValue = (ushort)Math.Round((dataPoint.Value - decompressionOffset) * compressionScale); + + if (compressedValue == NaNValue) + compressedValue--; + + if (double.IsNaN(dataPoint.Value)) + compressedValue = NaNValue; + + offset += LittleEndian.CopyBytes(compressedValue, result, offset); + } + + byte[] returnArray = GZipStream.CompressBuffer(result); + returnArray[0] = 0x44; + returnArray[1] = 0x33; + + return returnArray; + } + + private static byte[] ToDigitalData(List data, int seriesID) + { + List digitalData = new List(); + DigitalSection currentSection = null; + foreach (DataPoint dataPoint in data) + { + if (currentSection is null) + { + currentSection = new DigitalSection() + { + Start = dataPoint.Time, + End = dataPoint.Time, + Value = dataPoint.Value, + NumPoints = 1 + }; + } + else if (currentSection.Value != dataPoint.Value) + { + digitalData.Add(currentSection); + currentSection = new DigitalSection() + { + Start = dataPoint.Time, + End = dataPoint.Time, + Value = dataPoint.Value, + NumPoints = 1 + }; + } + else + { + currentSection.NumPoints++; + currentSection.End = dataPoint.Time; + } + } + + if (!(currentSection is null)) + digitalData.Add(currentSection); + + int totalByteLength = sizeof(int) + 2 * sizeof(double) + digitalData.Count * DigitalSection.Size; + byte[] result = new byte[totalByteLength]; + int offset = 0; + + const ushort MaxCompressedValue = ushort.MaxValue - 1; + double range = data.Select(item => item.Value).Max() - data.Select(item => item.Value).Min(); + double decompressionOffset = data.Select(item => item.Value).Min(); + double decompressionScale = range / MaxCompressedValue; + double compressionScale = (decompressionScale != 0.0D) ? 1.0D / decompressionScale : 0.0D; + + offset += LittleEndian.CopyBytes(seriesID, result, offset); + offset += LittleEndian.CopyBytes(decompressionOffset, result, offset); + offset += LittleEndian.CopyBytes(decompressionScale, result, offset); + + foreach (DigitalSection digitalSection in digitalData) + offset += digitalSection.CopyBytes(result, offset, compressionScale, decompressionOffset); + + byte[] returnArray = GZipStream.CompressBuffer(result); + returnArray[0] = DigitalHeader[0]; + returnArray[1] = DigitalHeader[1]; + return returnArray; + } + + /// + /// Decompresses a byte array into a List of DataPoints + /// + /// The byte array filled with compressed data + /// List of data series consisting of series ID and data points. + public static List>> Decompress(byte[] data) + { + List>> result = new List>>(); + + if (data == null) + return result; + // If the blob contains the GZip header, + // use the legacy deserialization algorithm + if (data[0] == LegacyHeader[0] && data[1] == LegacyHeader[1]) + { + return Decompress_Legacy(data); + } + // If this blob uses digital decompression use that algorithm + if (data[0] == DigitalHeader[0] && data[1] == DigitalHeader[1]) + { + return Decompress_Digital(data); + } + + // Restore the GZip header before uncompressing + data[0] = LegacyHeader[0]; + data[1] = LegacyHeader[1]; + + byte[] uncompressedData; + int offset; + + uncompressedData = GZipStream.UncompressBuffer(data); + offset = 0; + + int m_samples = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + List times = new List(); + + while (times.Count < m_samples) + { + int timeValues = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + long currentValue = LittleEndian.ToInt64(uncompressedData, offset); + offset += sizeof(long); + times.Add(new DateTime(currentValue)); + + for (int i = 1; i < timeValues; i++) + { + currentValue += LittleEndian.ToUInt16(uncompressedData, offset); + offset += sizeof(ushort); + times.Add(new DateTime(currentValue)); + } + } + + while (offset < uncompressedData.Length) + { + List dataSeries = new List(); + int seriesID = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + + const ushort NaNValue = ushort.MaxValue; + double decompressionOffset = LittleEndian.ToDouble(uncompressedData, offset); + double decompressionScale = LittleEndian.ToDouble(uncompressedData, offset + sizeof(double)); + offset += 2 * sizeof(double); + + for (int i = 0; i < m_samples; i++) + { + ushort compressedValue = LittleEndian.ToUInt16(uncompressedData, offset); + offset += sizeof(ushort); + + double decompressedValue = decompressionScale * compressedValue + decompressionOffset; + + if (compressedValue == NaNValue) + decompressedValue = double.NaN; + + dataSeries.Add(new DataPoint() + { + Time = times[i], + Value = decompressedValue + }); + } + + result.Add(new Tuple>(seriesID, dataSeries)); + } + + return result; + } + + private static List>> Decompress_Legacy(byte[] data) + { + List>> result = new List>>(); + byte[] uncompressedData; + int offset; + DateTime[] times; + int seriesID; + + uncompressedData = GZipStream.UncompressBuffer(data); + offset = 0; + + int m_samples = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + times = new DateTime[m_samples]; + + for (int i = 0; i < m_samples; i++) + { + times[i] = new DateTime(LittleEndian.ToInt64(uncompressedData, offset)); + offset += sizeof(long); + } + + while (offset < uncompressedData.Length) + { + seriesID = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + List points = new List(); + + for (int i = 0; i < m_samples; i++) + { + points.Add(new DataPoint() + { + Time = times[i], + Value = LittleEndian.ToDouble(uncompressedData, offset) + }); + + offset += sizeof(double); + } + + result.Add(new Tuple>(seriesID, points)); + } + return result; + } + + /// + /// Decompresses a Digital stored as compresed series of changes + /// + /// The compressed + /// a Dictionary mapping a SeriesID to a decopmressed + private static List>> Decompress_Digital(byte[] data) + { + List>> result = new List>>(); + byte[] uncompressedData; + int offset; + int seriesID; + List points = new List(); + + // Restore the GZip header before uncompressing + data[0] = LegacyHeader[0]; + data[1] = LegacyHeader[1]; + + uncompressedData = GZipStream.UncompressBuffer(data); + offset = 0; + + seriesID = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + double decompressionOffset = LittleEndian.ToDouble(uncompressedData, offset); + double decompressionScale = LittleEndian.ToDouble(uncompressedData, offset + sizeof(double)); + offset += 2 * sizeof(double); + + while(offset < uncompressedData.Length) + { + DigitalSection section = DigitalSection.FromBytes(uncompressedData, offset, decompressionOffset, decompressionScale); + offset += DigitalSection.Size; + + points.Add(new DataPoint() + { + Time = section.Start, + Value = section.Value + }); + + if (section.NumPoints == 1) + continue; + + // Use a fixed-point offset with 6 bits of additional + // precision to help avoid accumulation of rounding errors + long diff = (section.End - section.Start).Ticks << 6; + long step = diff / (section.NumPoints - 1); + long lastOffset = step; + + for (int i = 1; i < section.NumPoints - 1; i++) + { + points.Add(new DataPoint() + { + Time = section.Start.AddTicks(lastOffset >> 6), + Value = section.Value + }); + + lastOffset += step; + } + + points.Add(new DataPoint() + { + Time = section.End, + Value = section.Value + }); + } + + result.Add(new Tuple>(seriesID, points)); + + return result; + } + + private static HashSet QueryChannelsWithData(AdoDataConnection connection, Event evt) + { + const string FilterQueryFormat = + "SELECT Series.ChannelID " + + "FROM " + + " ChannelData JOIN " + + " Series ON ChannelData.SeriesID = Series.ID JOIN " + + " Event ON ChannelData.EventID = Event.ID " + + "WHERE " + + " Event.FileGroupID = {0} AND " + + " Event.StartTime = {1}"; + + object startTime2 = ToDateTime2(connection, evt.StartTime); + + using (DataTable table = connection.RetrieveData(FilterQueryFormat, evt.FileGroupID, startTime2)) + { + IEnumerable channelsWithData = table + .AsEnumerable() + .Select(row => row.ConvertField("ChannelID")); + + return new HashSet(channelsWithData); + } + } + + private static object ToDateTime2(AdoDataConnection connection, DateTime dateTime) + { + using (IDbCommand command = connection.Connection.CreateCommand()) + { + IDbDataParameter parameter = command.CreateParameter(); + parameter.DbType = DbType.DateTime2; + parameter.Value = dateTime; + return parameter; + } + } + + /// + /// The header of a datablob compressed as analog Data + /// + public static readonly byte[] AnalogHeader = { 0x11, 0x11 }; + + /// + /// The header of a datablob compressed as Digital State Changes + /// + public static readonly byte[] DigitalHeader = { 0x22, 0x22 }; + + /// + /// The header of a datablob compressed as Legacy Data + /// + public static readonly byte[] LegacyHeader = { 0x1F, 0x8B }; + + #endregion + } +} diff --git a/src/Libraries/openXDA.Model/Channels/DataPoint.cs b/src/Libraries/openXDA.Model/Channels/DataPoint.cs new file mode 100644 index 00000000..88c39332 --- /dev/null +++ b/src/Libraries/openXDA.Model/Channels/DataPoint.cs @@ -0,0 +1,110 @@ +//****************************************************************************************************** +// DataPoint.cs - Gbtc +// +// Copyright © 2025, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/15/2025 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace openXDA.Model +{ + /// + /// Represents a single data point in a time series. + /// + public class DataPoint + { + #region [ Properties ] + + public DateTime Time { get; set; } + public double Value { get; set; } + + #endregion + + #region [ Methods ] + + public DataPoint Shift(TimeSpan timeShift) + { + return new DataPoint() + { + Time = Time.Add(timeShift), + Value = Value + }; + } + + public DataPoint Negate() + { + return new DataPoint() + { + Time = Time, + Value = -Value + }; + } + + public DataPoint Add(DataPoint point) + { + if (Time != point.Time) + throw new InvalidOperationException("Cannot add datapoints with mismatched times"); + + return new DataPoint() + { + Time = Time, + Value = Value + point.Value + }; + } + + public DataPoint Subtract(DataPoint point) + { + return Add(point.Negate()); + } + + public DataPoint Add(double value) + { + return new DataPoint() + { + Time = Time, + Value = Value + value + }; + } + + public DataPoint Subtract(double value) + { + return Add(-value); + } + + public DataPoint Multiply(double value) + { + return new DataPoint() + { + Time = Time, + Value = Value * value + }; + } + + public bool LargerThan(double comparison) + { + return Value > comparison; + } + + public bool LargerThan(DataPoint point) + { + return LargerThan(point.Value); + } + + #endregion + } +} diff --git a/src/Libraries/openXDA.Model/Channels/MeasurementCharacteristic.cs b/src/Libraries/openXDA.Model/Channels/MeasurementCharacteristic.cs new file mode 100644 index 00000000..a3e49898 --- /dev/null +++ b/src/Libraries/openXDA.Model/Channels/MeasurementCharacteristic.cs @@ -0,0 +1,45 @@ +//****************************************************************************************************** +// MeasurementCharacteristic.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel.DataAnnotations; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [PostRoles("Administrator, Transmission SME")] + [DeleteRoles("Administrator, Transmission SME")] + [PatchRoles("Administrator, Transmission SME")] + public class MeasurementCharacteristic + { + [PrimaryKey(true)] + public int ID { get; set; } + + [StringLength(200)] + [DefaultSortOrder] + public string Name { get; set; } + + public string Description { get; set; } + + public bool Display { get; set; } + } +} diff --git a/src/Libraries/openXDA.Model/Channels/MeasurementType.cs b/src/Libraries/openXDA.Model/Channels/MeasurementType.cs new file mode 100644 index 00000000..20d3ea30 --- /dev/null +++ b/src/Libraries/openXDA.Model/Channels/MeasurementType.cs @@ -0,0 +1,44 @@ +//****************************************************************************************************** +// MeasurementType.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.ComponentModel.DataAnnotations; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [PostRoles("Administrator, Transmission SME")] + [DeleteRoles("Administrator, Transmission SME")] + [PatchRoles("Administrator, Transmission SME")] + public class MeasurementType + { + [PrimaryKey(true)] + public int ID { get; set; } + + [StringLength(200)] + [DefaultSortOrder] + public string Name { get; set; } + + public string Description { get; set; } + } +} diff --git a/src/Libraries/openXDA.Model/Channels/Phase.cs b/src/Libraries/openXDA.Model/Channels/Phase.cs new file mode 100644 index 00000000..705ec274 --- /dev/null +++ b/src/Libraries/openXDA.Model/Channels/Phase.cs @@ -0,0 +1,43 @@ +//****************************************************************************************************** +// Phase.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel.DataAnnotations; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [PostRoles("Administrator, Transmission SME")] + [DeleteRoles("Administrator, Transmission SME")] + [PatchRoles("Administrator, Transmission SME")] + public class Phase + { + [PrimaryKey(true)] + public int ID { get; set; } + + [StringLength(200)] + [DefaultSortOrder] + public string Name { get; set; } + + public string Description { get; set; } + } +} diff --git a/src/Libraries/openXDA.Model/Channels/Series.cs b/src/Libraries/openXDA.Model/Channels/Series.cs new file mode 100644 index 00000000..d6c96a6c --- /dev/null +++ b/src/Libraries/openXDA.Model/Channels/Series.cs @@ -0,0 +1,248 @@ +//****************************************************************************************************** +// Series.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 06/20/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Data; +using Gemstone.Data; +using Gemstone.Data.Model; +using Newtonsoft.Json; + +namespace openXDA.Model +{ + public class SeriesKey : IEquatable + { + #region [ Constructors ] + + public SeriesKey(ChannelKey channelKey, string seriesType) + { + ChannelKey = channelKey; + SeriesType = seriesType; + } + + public SeriesKey(Series series) + : this(new ChannelKey(series.Channel), series.SeriesType.Name) + { + } + + #endregion + + #region [ Properties ] + + public ChannelKey ChannelKey { get; } + public string SeriesType { get; } + + #endregion + + #region [ Methods ] + + public Series Find(AdoDataConnection connection, int meterID) + { + const string QueryFormat = + "SELECT Series.* " + + "FROM " + + " Series JOIN " + + " Channel ON Series.ChannelID = Channel.ID JOIN " + + " MeasurementType ON Channel.MeasurementTypeID = MeasurementType.ID JOIN " + + " MeasurementCharacteristic ON Channel.MeasurementCharacteristicID = MeasurementCharacteristic.ID JOIN " + + " Phase ON Channel.PhaseID = Phase.ID JOIN " + + " SeriesType ON Series.SeriesTypeID = SeriesType.ID " + + "WHERE " + + " Channel.MeterID = {0} AND " + + " Channel.AssetID = {1} AND " + + " Channel.HarmonicGroup = {2} AND " + + " Channel.Name = {3} AND " + + " MeasurementType.Name = {4} AND " + + " MeasurementCharacteristic.Name = {5} AND " + + " Phase.Name = {6} AND " + + " SeriesType.Name = {7}"; + + object[] parameters = + { + meterID, + ChannelKey.LineID, + ChannelKey.HarmonicGroup, + ChannelKey.Name, + ChannelKey.MeasurementType, + ChannelKey.MeasurementCharacteristic, + ChannelKey.Phase, + SeriesType + }; + + using (DataTable table = connection.RetrieveData(QueryFormat, parameters)) + { + if (table.Rows.Count == 0) + return null; + + TableOperations seriesTable = new TableOperations(connection); + return seriesTable.LoadRecord(table.Rows[0]); + } + } + + public override int GetHashCode() + { + StringComparer stringComparer = StringComparer.OrdinalIgnoreCase; + + int hash = 1009; + hash = 9176 * hash + ChannelKey.GetHashCode(); + hash = 9176 * hash + stringComparer.GetHashCode(SeriesType); + return hash; + } + + public override bool Equals(object obj) + { + return Equals(obj as SeriesKey); + } + + public bool Equals(SeriesKey other) + { + if (other is null) + return false; + + StringComparison stringComparison = StringComparison.OrdinalIgnoreCase; + + return + ChannelKey.Equals(other.ChannelKey) && + SeriesType.Equals(other.SeriesType, stringComparison); + } + + #endregion + } + + public class Series + { + #region [ Members ] + + // Fields + private SeriesType m_seriesType; + private Channel m_channel; + + #endregion + + #region [ Properties ] + + [PrimaryKey(true)] + public int ID { get; set; } + + public int ChannelID { get; set; } + + public int SeriesTypeID { get; set; } + + public string SourceIndexes { get; set; } + + [JsonIgnore] + [NonRecordField] + public SeriesType SeriesType + { + get + { + if (m_seriesType is null) + m_seriesType = LazyContext.GetSeriesType(SeriesTypeID); + + if (m_seriesType is null) + m_seriesType = QuerySeriesType(); + + return m_seriesType; + } + set => m_seriesType = value; + } + + [JsonIgnore] + [NonRecordField] + public Channel Channel + { + get + { + if (m_channel is null) + m_channel = LazyContext.GetChannel(ChannelID); + + if (m_channel is null) + m_channel = QueryChannel(); + + return m_channel; + } + set => m_channel = value; + } + + [JsonIgnore] + [NonRecordField] + public Func ConnectionFactory + { + get => LazyContext.ConnectionFactory; + set => LazyContext.ConnectionFactory = value; + } + + [JsonIgnore] + [NonRecordField] + internal LazyContext LazyContext { get; set; } = new LazyContext(); + + #endregion + + #region [ Methods ] + + public SeriesType GetSeriesType(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations seriesTypeTable = new TableOperations(connection); + return seriesTypeTable.QueryRecordWhere("ID = {0}", SeriesTypeID); + } + + public Channel GetChannel(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations channelTable = new TableOperations(connection); + return channelTable.QueryRecordWhere("ID = {0}", ChannelID); + } + + private SeriesType QuerySeriesType() + { + SeriesType seriesType; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + seriesType = GetSeriesType(connection); + } + + return LazyContext.GetSeriesType(seriesType); + } + + private Channel QueryChannel() + { + Channel channel; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + channel = GetChannel(connection); + } + + if ((object)channel != null) + channel.LazyContext = LazyContext; + + return LazyContext.GetChannel(channel); + } + + #endregion + } +} diff --git a/src/Libraries/openXDA.Model/Channels/SeriesType.cs b/src/Libraries/openXDA.Model/Channels/SeriesType.cs new file mode 100644 index 00000000..c7621b5d --- /dev/null +++ b/src/Libraries/openXDA.Model/Channels/SeriesType.cs @@ -0,0 +1,40 @@ +//****************************************************************************************************** +// SeriesType.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel.DataAnnotations; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [TableName("SeriesType")] + public class SeriesType + { + [PrimaryKey(true)] + public int ID { get; set; } + + [StringLength(200)] + public string Name { get; set; } + + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/Events/BreakerRestrike.cs b/src/Libraries/openXDA.Model/Events/BreakerRestrike.cs new file mode 100644 index 00000000..e76661d5 --- /dev/null +++ b/src/Libraries/openXDA.Model/Events/BreakerRestrike.cs @@ -0,0 +1,65 @@ +//****************************************************************************************************** +// BreakerRestrike.cs - Gbtc +// +// Copyright © 2019, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/30/2019 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Data; +using Gemstone.Data; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + public class BreakerRestrike + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int EventID { get; set; } + + public int PhaseID { get; set; } + + public int InitialExtinguishSample { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime InitialExtinguishTime { get; set; } + public double InitialExtinguishVoltage { get; set; } + public int RestrikeSample { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime RestrikeTime { get; set; } + public double RestrikeVoltage { get; set; } + public double RestrikeCurrentPeak { get; set; } + public double RestrikeVoltageDip { get; set; } + public int TransientPeakSample { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime TransientPeakTime { get; set; } + public double TransientPeakVoltage { get; set; } + public double PerUnitTransientPeakVoltage { get; set; } + public int FinalExtinguishSample { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime FinalExtinguishTime { get; set; } + public double FinalExtinguishVoltage { get; set; } + public double I2t { get; set; } + + } +} diff --git a/src/Libraries/openXDA.Model/Events/Disturbances/Disturbance.cs b/src/Libraries/openXDA.Model/Events/Disturbances/Disturbance.cs new file mode 100644 index 00000000..c83ce0e6 --- /dev/null +++ b/src/Libraries/openXDA.Model/Events/Disturbances/Disturbance.cs @@ -0,0 +1,67 @@ +//****************************************************************************************************** +// Disturbance.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + public class Disturbance + { + [PrimaryKey(true)] + public int ID { get; set; } + public int EventID { get; set; } + public int EventTypeID { get; set; } + public int PhaseID { get; set; } + public double Magnitude { get; set; } + public double PerUnitMagnitude { get; set; } + + [FieldDataType(System.Data.DbType.DateTime2, Gemstone.Data.DatabaseType.SQLServer)] + public DateTime StartTime { get; set; } + + [FieldDataType(System.Data.DbType.DateTime2, Gemstone.Data.DatabaseType.SQLServer)] + public DateTime EndTime { get; set; } + + public double DurationSeconds { get; set; } + public double DurationCycles { get; set; } + public int StartIndex { get; set; } + public int EndIndex { get; set; } + public string UpdatedBy { get; set; } + } + + [TableName("DisturbanceView")] + public class DisturbanceView: Disturbance + { + public int MeterID { get; set; } + public int LineID { get; set; } + public int? SeverityCode { get; set; } + public string MeterName { get; set; } + public string PhaseName { get; set; } + } + + [TableName("DisturbanceView")] + public class DisturbancesForDay : DisturbanceView { } + + [TableName("DisturbanceView")] + public class DisturbancesForMeter : DisturbanceView { } +} \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/Events/Event.cs b/src/Libraries/openXDA.Model/Events/Event.cs new file mode 100644 index 00000000..992ad47e --- /dev/null +++ b/src/Libraries/openXDA.Model/Events/Event.cs @@ -0,0 +1,91 @@ +//****************************************************************************************************** +// Event.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Data; +using Gemstone.Data; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [TableName("Event")] + public class Event + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int FileGroupID { get; set; } + + public int MeterID { get; set; } + + public int AssetID { get; set; } + + public int EventTypeID { get; set; } + + public int? EventDataID { get; set; } + + public string Name { get; set; } + + public string Alias { get; set; } + + public string ShortName { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime StartTime { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime EndTime { get; set; } + + public int Samples { get; set; } + + public int TimeZoneOffset { get; set; } + + public int SamplesPerSecond { get; set; } + + public int SamplesPerCycle { get; set; } + + public string Description { get; set; } + + public int FileVersion { get; set; } + + public string UpdatedBy { get; set; } + } + + [TableName("EventView")] + public class EventView : Event + { + [PrimaryKey(true)] + public new int ID + { + get => base.ID; + set => base.ID = value; + } + + public string AssetName { get; set; } + + public string MeterName { get; set; } + + public string StationName { get; set; } + + public string EventTypeName { get; set; } + } +} \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/Events/EventStat.cs b/src/Libraries/openXDA.Model/Events/EventStat.cs new file mode 100644 index 00000000..6e004a3e --- /dev/null +++ b/src/Libraries/openXDA.Model/Events/EventStat.cs @@ -0,0 +1,57 @@ +//****************************************************************************************************** +// EventStat.cs - Gbtc +// +// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 11/07/2018 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + public class EventStat + { + [PrimaryKey(true)] + public int ID { get; set; } + public int EventID { get; set; } + public double? VPeak { get; set; } + public double? VAMax { get; set; } + public double? VBMax { get; set; } + public double? VCMax { get; set; } + public double? VABMax { get; set; } + public double? VBCMax { get; set; } + public double? VCAMax { get; set; } + public double? VAMin { get; set; } + public double? VBMin { get; set; } + public double? VCMin { get; set; } + public double? VABMin { get; set; } + public double? VBCMin { get; set; } + public double? VCAMin { get; set; } + public double? IPeak { get; set; } + public double? IAMax { get; set; } + public double? IBMax { get; set; } + public double? ICMax { get; set; } + public double? IA2t { get; set; } + public double? IB2t { get; set; } + public double? IC2t { get; set; } + public double? InitialMW { get; set; } + public double? FinalMW { get; set; } + public int? PQViewID { get; set; } + } +} diff --git a/src/Libraries/openXDA.Model/Events/Faults/Fault.cs b/src/Libraries/openXDA.Model/Events/Faults/Fault.cs new file mode 100644 index 00000000..dbf8b955 --- /dev/null +++ b/src/Libraries/openXDA.Model/Events/Faults/Fault.cs @@ -0,0 +1,137 @@ +//****************************************************************************************************** +// Fault.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [TableName("FaultSummary")] + public class Fault + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int EventID { get; set; } + + public string Algorithm { get; set; } + + public int FaultNumber { get; set; } + + public int CalculationCycle { get; set; } + + public double Distance { get; set; } + + public int PathNumber { get; set; } + + public int LineSegmentID { get; set; } + + public double LineSegmentDistance { get; set; } + + public double CurrentMagnitude { get; set; } + + public double CurrentLag { get; set; } + + public double PrefaultCurrent { get; set; } + + public double PostfaultCurrent { get; set; } + + public double ReactanceRatio { get; set; } + + [FieldDataType(System.Data.DbType.DateTime2, Gemstone.Data.DatabaseType.SQLServer)] + public DateTime Inception { get; set; } + + public double DurationSeconds { get; set; } + + public double DurationCycles { get; set; } + + public string FaultType { get; set; } + + public bool IsSelectedAlgorithm { get; set; } + + public bool IsValid { get; set; } + + public bool IsSuppressed { get; set; } + } + + public class FaultSummary : Fault { } + + [TableName("FaultView")] + public class FaultView : Fault + { + public string MeterName { get; set; } + + public string ShortName { get; set; } + + public string LocationName { get; set; } + + public int MeterID { get; set; } + + public int LineID { get; set; } + + public string LineName { get; set; } + + public int Voltage { get; set; } + + public DateTime InceptionTime { get; set; } + + public double CurrentDistance { get; set; } + + public int RK { get; set; } + } + + [TableName("FaultView")] + public class FaultForMeter: FaultView { } + + public class FaultsDetailsByDate + { + public int thefaultid { get; set; } + + public string thesite { get; set; } + + public string locationname { get; set; } + + public int themeterid { get; set; } + + public int thelineid { get; set; } + + public int theeventid { get; set; } + + public string thelinename { get; set; } + + public int voltage { get; set; } + + public string theinceptiontime { get; set; } + + public string thefaulttype { get; set; } + + public double thecurrentdistance { get; set; } + + public int notecount { get; set; } + + public int rk { get; set; } + + [NonRecordField] + public string theeventtype { get; set; } + } +} \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/Events/Faults/FaultCurve.cs b/src/Libraries/openXDA.Model/Events/Faults/FaultCurve.cs new file mode 100644 index 00000000..02268fa2 --- /dev/null +++ b/src/Libraries/openXDA.Model/Events/Faults/FaultCurve.cs @@ -0,0 +1,250 @@ +//****************************************************************************************************** +// FaultCurve.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 09/06/2017 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel.DataAnnotations; +using Gemstone; +using Gemstone.Data.Model; +using Ionic.Zlib; + +namespace openXDA.Model +{ + public class FaultCurve + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int EventID { get; set; } + + public int PathNumber { get; set; } + + [StringLength(200)] + public string Algorithm { get; set; } + + public byte[] Data { get; set; } + + public byte[] AngleData { get; set; } + + #region [Private Class] + private class DataPoint + { + public DateTime Time; + public double Value; + } + + #endregion + + #region [Methods] + + public void Adjust(Ticks ticks) + { + // If the blob contains the GZip header, + // move from Legacy Compression to normal Compression + if (this.Data[0] == 0x1F && this.Data[1] == 0x8B) + { + this.Data = MigrateCompression(this.Data); + } + + // If the blob contains the GZip header, + // move from Legacy Compression to normal Compression + if (this.AngleData[0] == 0x1F && this.AngleData[1] == 0x8B) + { + this.AngleData = MigrateCompression(this.AngleData); + } + + this.Data = ChangeTS(this.Data, ticks); + this.AngleData = ChangeTS(this.AngleData, ticks); + + + } + + private static byte[] ChangeTS(byte[] data, Ticks ticks) + { + data[0] = 0x1F; + data[1] = 0x8B; + + byte[] uncompressedData = GZipStream.UncompressBuffer(data); + byte[] resultData = new byte[uncompressedData.Length]; + + uncompressedData.CopyTo(resultData,0); + + int offset = 0; + + int m_samples = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + int timeValues = LittleEndian.ToInt32(uncompressedData, offset); + + int startTS = offset; + + offset += sizeof(int); + + long currentValue = LittleEndian.ToInt64(uncompressedData, offset); + + DateTime startTime = new DateTime(currentValue); + startTime = startTime.AddTicks(ticks); + + LittleEndian.CopyBytes(startTime.Ticks, resultData, startTS); + + resultData = GZipStream.CompressBuffer(resultData); + resultData[0] = 0x44; + resultData[1] = 0x33; + return resultData; + } + + private static byte[] MigrateCompression(byte[] data) + { + byte[] uncompressedData; + int offset; + DateTime[] times; + List series; + int seriesID = 0; + + uncompressedData = GZipStream.UncompressBuffer(data); + offset = 0; + + int m_samples = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + times = new DateTime[m_samples]; + + for (int i = 0; i < m_samples; i++) + { + times[i] = new DateTime(LittleEndian.ToInt64(uncompressedData, offset)); + offset += sizeof(long); + } + + series = new List(); + + while (offset < uncompressedData.Length) + { + + seriesID = LittleEndian.ToInt32(uncompressedData, offset); + offset += sizeof(int); + + + for (int i = 0; i < m_samples; i++) + { + series.Add(new DataPoint() + { + Time = times[i], + Value = LittleEndian.ToDouble(uncompressedData, offset) + }); + + offset += sizeof(double); + } + } + + var timeSeries = series.Select(dataPoint => new { Time = dataPoint.Time.Ticks, Compressed = false }).ToList(); + + for (int i = 1; i < timeSeries.Count; i++) + { + long previousTimestamp = series[i - 1].Time.Ticks; + long timestamp = timeSeries[i].Time; + long diff = timestamp - previousTimestamp; + + if (diff >= 0 && diff <= ushort.MaxValue) + timeSeries[i] = new { Time = diff, Compressed = true }; + + + } + + int timeSeriesByteLength = timeSeries.Sum(obj => obj.Compressed ? sizeof(ushort) : sizeof(int) + sizeof(long)); + int dataSeriesByteLength = sizeof(int) + (2 * sizeof(double)) + (m_samples * sizeof(ushort)); + int totalByteLength = sizeof(int) + timeSeriesByteLength + dataSeriesByteLength; + + + byte[] result = new byte[totalByteLength]; + offset = 0; + + offset += LittleEndian.CopyBytes(m_samples, result, offset); + + List uncompressedIndexes = timeSeries + .Select((obj, Index) => new { obj.Compressed, Index }) + .Where(obj => !obj.Compressed) + .Select(obj => obj.Index) + .ToList(); + + for (int i = 0; i < uncompressedIndexes.Count; i++) + { + int index = uncompressedIndexes[i]; + int nextIndex = (i + 1 < uncompressedIndexes.Count) ? uncompressedIndexes[i + 1] : timeSeries.Count; + + offset += LittleEndian.CopyBytes(nextIndex - index, result, offset); + offset += LittleEndian.CopyBytes(timeSeries[index].Time, result, offset); + + for (int j = index + 1; j < nextIndex; j++) + offset += LittleEndian.CopyBytes((ushort)timeSeries[j].Time, result, offset); + } + + const ushort NaNValue = ushort.MaxValue; + const ushort MaxCompressedValue = ushort.MaxValue - 1; + double range = series.Select(item => item.Value).Max() - series.Select(item => item.Value).Min(); + double decompressionOffset = series.Select(item => item.Value).Min(); + double decompressionScale = range / MaxCompressedValue; + double compressionScale = (decompressionScale != 0.0D) ? 1.0D / decompressionScale : 0.0D; + + offset += LittleEndian.CopyBytes(seriesID, result, offset); + offset += LittleEndian.CopyBytes(decompressionOffset, result, offset); + offset += LittleEndian.CopyBytes(decompressionScale, result, offset); + + foreach (DataPoint dataPoint in series) + { + ushort compressedValue = (ushort)Math.Round((dataPoint.Value - decompressionOffset) * compressionScale); + + if (compressedValue == NaNValue) + compressedValue--; + + if (double.IsNaN(dataPoint.Value)) + compressedValue = NaNValue; + + offset += LittleEndian.CopyBytes(compressedValue, result, offset); + } + + byte[] returnArray = GZipStream.CompressBuffer(result); + returnArray[0] = 0x44; + returnArray[1] = 0x33; + + return returnArray; + + } + #endregion + } + + public class FaultCurveStatistic + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int FaultCurveID { get; set; } + + public int FaultNumber { get; set; } + + public double Maximum { get; set; } + + public double Minimum { get; set; } + + public double Average { get; set; } + + public double StandardDeviation { get; set; } + } +} diff --git a/src/Libraries/openXDA.Model/Events/RelayPerformance.cs b/src/Libraries/openXDA.Model/Events/RelayPerformance.cs new file mode 100644 index 00000000..4950235e --- /dev/null +++ b/src/Libraries/openXDA.Model/Events/RelayPerformance.cs @@ -0,0 +1,63 @@ +//****************************************************************************************************** +// RelayPerformance.cs - Gbtc +// +// Copyright © 2019, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 07/10/2019 - Christoph Lackner +// Generated original version of source code. +// 08/20/2021 - Christoph Lackner +// Added additional Trip Coil Curve points. +// +//****************************************************************************************************** + +using System.Data; +using Gemstone.Data; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + public class RelayPerformance + { + [PrimaryKey(true)] + public int ID { get; set; } + public int EventID { get; set; } + public int ChannelID { get; set; } + public double? Imax1 { get; set; } + public int? Tmax1 { get; set; } + public double? Imax2 { get; set; } + public int? TplungerLatch { get; set; } + public double IplungerLatch { get; set; } + public double? Idrop { get; set; } + public int? TiDrop { get; set; } + public int? Tend { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime? TripInitiate { get; set; } + public int? TripTime { get; set; } + public int? PickupTime { get; set; } + public double? TripTimeCurrent { get; set;} + public double? PickupTimeCurrent { get; set; } + public double? TripCoilCondition { get; set; } + public int TripCoilConditionTime { get; set; } + public int? ExtinctionTimeA { get; set; } + public int? ExtinctionTimeB { get; set; } + public int? ExtinctionTimeC { get; set; } + public double? I2CA { get; set; } + public double? I2CB { get; set; } + public double? I2CC { get; set; } + + } +} diff --git a/src/Libraries/openXDA.Model/Files/DataFile.cs b/src/Libraries/openXDA.Model/Files/DataFile.cs new file mode 100644 index 00000000..10b9d4b5 --- /dev/null +++ b/src/Libraries/openXDA.Model/Files/DataFile.cs @@ -0,0 +1,93 @@ +//****************************************************************************************************** +// DataFile.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Text; +using Gemstone.Data.Model; +using Gemstone.IO.Checksums; +using Newtonsoft.Json; + +namespace openXDA.Model +{ + [Serializable] + public class DataFile + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int FileGroupID { get; set; } + + public string FilePath { get; set; } + + public int FilePathHash { get; set; } + + public long FileSize { get; set; } + + public DateTime CreationTime { get; set; } + + public DateTime LastWriteTime { get; set; } + + public DateTime LastAccessTime { get; set; } + + [NonRecordField] + [JsonIgnore] + public FileBlob FileBlob { get; set; } + + public static int GetHash(string filePath) + { + Encoding utf8 = new UTF8Encoding(false); + byte[] pathData = utf8.GetBytes(filePath); + return unchecked((int)Crc32.Compute(pathData, 0, pathData.Length)); + } + } + + [TableName("DataFile")] + public class DataFileDb : DataFile { } + + public static partial class TableOperationsExtensions + { + public static DataFile QueryDataFile(this TableOperations dataFileTable, string filePath) + { + int hashCode = DataFile.GetHash(filePath); + DataFile dataFile = QueryDataFile(dataFileTable, filePath, hashCode); + + if (dataFile != null) + return dataFile; + + int legacyHashCode = filePath.GetHashCode(); + dataFile = QueryDataFile(dataFileTable, filePath, legacyHashCode); + + if (dataFile == null) + return null; + + dataFile.FilePathHash = hashCode; + dataFileTable.UpdateRecord(dataFile); + return dataFile; + } + + private static DataFile QueryDataFile(TableOperations dataFileTable, string filePath, int hashCode) + { + IEnumerable dataFiles = dataFileTable.QueryRecordsWhere("FilePathHash = {0}", hashCode); + return dataFiles.FirstOrDefault(dataFile => dataFile.FilePath == filePath); + } + } +} diff --git a/src/Libraries/openXDA.Model/Files/FileBlob.cs b/src/Libraries/openXDA.Model/Files/FileBlob.cs new file mode 100644 index 00000000..00beeef8 --- /dev/null +++ b/src/Libraries/openXDA.Model/Files/FileBlob.cs @@ -0,0 +1,38 @@ +//****************************************************************************************************** +// FileBlob.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [Serializable] + public class FileBlob + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int DataFileID { get; set; } + + public byte[] Blob { get; set; } + } +} \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/Files/FileGroup.cs b/src/Libraries/openXDA.Model/Files/FileGroup.cs new file mode 100644 index 00000000..d6c50ab2 --- /dev/null +++ b/src/Libraries/openXDA.Model/Files/FileGroup.cs @@ -0,0 +1,102 @@ +//****************************************************************************************************** +// FileGroup.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [Serializable] + public class FileGroup + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int MeterID { get; set; } + + [FieldDataType(System.Data.DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime DataStartTime { get; set; } + + [FieldDataType(System.Data.DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime DataEndTime { get; set; } + + [FieldDataType(System.Data.DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime ProcessingStartTime { get; set; } + + [FieldDataType(System.Data.DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime ProcessingEndTime { get; set; } + + public int ProcessingVersion { get; set; } + + public int ProcessingStatus { get; set; } + + [NonRecordField] + public List DataFiles { get; set; } = new List(); + + public void AddFieldValue(AdoDataConnection connection, string name, string value, string description = null) + { + TableOperations fileGroupFieldTable = new TableOperations(connection); + FileGroupField fileGroupField = fileGroupFieldTable.GetOrAdd(name, description); + + TableOperations fileGroupFieldValueTable = new TableOperations(connection); + FileGroupFieldValue fileGroupFieldValue = new FileGroupFieldValue(); + fileGroupFieldValue.FileGroupID = ID; + fileGroupFieldValue.FileGroupFieldID = fileGroupField.ID; + fileGroupFieldValue.Value = value; + fileGroupFieldValueTable.AddNewRecord(fileGroupFieldValue); + } + + public void AddOrUpdateFieldValue(AdoDataConnection connection, string name, string value, string description = null) + { + TableOperations fileGroupFieldTable = new TableOperations(connection); + FileGroupField fileGroupField = fileGroupFieldTable.GetOrAdd(name, description); + + TableOperations fileGroupFieldValueTable = new TableOperations(connection); + RecordRestriction fileGroupRestriction = new RecordRestriction("FileGroupID = {0}", ID); + RecordRestriction fileGroupFieldRestriction = new RecordRestriction("FileGroupFieldID = {0}", fileGroupField.ID); + RecordRestriction queryRestriction = fileGroupRestriction & fileGroupFieldRestriction; + + FileGroupFieldValue fileGroupFieldValue = fileGroupFieldValueTable.QueryRecord(queryRestriction) ?? new FileGroupFieldValue() + { + FileGroupID = ID, + FileGroupFieldID = fileGroupField.ID + }; + + fileGroupFieldValue.Value = value; + fileGroupFieldValueTable.AddNewOrUpdateRecord(fileGroupFieldValue); + } + } + + /// + /// Number indicating the processing status of a file group. + /// + public enum FileGroupProcessingStatus + { + Created = 0, + Queued = 1, + Processing = 2, + Success = 3, + Failed = 4, + PartialSuccess = 5 + } +} diff --git a/src/Libraries/openXDA.Model/Files/FileGroupField.cs b/src/Libraries/openXDA.Model/Files/FileGroupField.cs new file mode 100644 index 00000000..31021f88 --- /dev/null +++ b/src/Libraries/openXDA.Model/Files/FileGroupField.cs @@ -0,0 +1,61 @@ +//****************************************************************************************************** +// FileGroupField.cs - Gbtc +// +// Copyright © 2019, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 06/18/2019 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.ComponentModel.DataAnnotations; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + public class FileGroupField + { + [PrimaryKey(true)] + public int ID { get; set; } + + [StringLength(200)] + public string Name { get; set; } + + public string Description { get; set; } + } + + public static partial class TableOperationsExtensions + { + public static FileGroupField GetOrAdd(this TableOperations fileGroupFieldTable, string name, string description = null) + { + FileGroupField fileGroupField = fileGroupFieldTable.QueryRecordWhere("Name = {0}", name); + + if ((object)fileGroupField == null) + { + fileGroupField = new FileGroupField(); + fileGroupField.Name = name; + fileGroupField.Description = description; + + fileGroupFieldTable.AddNewRecord(fileGroupField); + + fileGroupField.ID = fileGroupFieldTable.Connection.ExecuteScalar("SELECT @@IDENTITY"); + } + + return fileGroupField; + } + } +} diff --git a/src/OpenSEE/App_Start/FilterConfig.cs b/src/Libraries/openXDA.Model/Files/FileGroupFieldValue.cs similarity index 73% rename from src/OpenSEE/App_Start/FilterConfig.cs rename to src/Libraries/openXDA.Model/Files/FileGroupFieldValue.cs index 97498ed4..55140bae 100644 --- a/src/OpenSEE/App_Start/FilterConfig.cs +++ b/src/Libraries/openXDA.Model/Files/FileGroupFieldValue.cs @@ -1,7 +1,7 @@ //****************************************************************************************************** -// FilterConfig.cs - Gbtc +// FileGroupFieldValue.cs - Gbtc // -// Copyright © 2020, Grid Protection Alliance. All Rights Reserved. +// Copyright © 2019, 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. @@ -16,21 +16,24 @@ // // Code Modification History: // ---------------------------------------------------------------------------------------------------- -// 02/19/2020 - Billy Ernest +// 06/18/2019 - Stephen C. Wills // Generated original version of source code. // //****************************************************************************************************** -using System.Web; -using System.Web.Mvc; +using Gemstone.Data.Model; -namespace OpenSEE +namespace openXDA.Model { - public class FilterConfig + public class FileGroupFieldValue { - public static void RegisterGlobalFilters(GlobalFilterCollection filters) - { - filters.Add(new HandleErrorAttribute()); - } + [PrimaryKey(true)] + public int ID { get; set; } + + public int FileGroupID { get; set; } + + public int FileGroupFieldID { get; set; } + + public string Value { get; set; } } } diff --git a/src/Libraries/openXDA.Model/LazyContext.cs b/src/Libraries/openXDA.Model/LazyContext.cs new file mode 100644 index 00000000..6f5c7acc --- /dev/null +++ b/src/Libraries/openXDA.Model/LazyContext.cs @@ -0,0 +1,369 @@ +//****************************************************************************************************** +// LazyContext.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 09/04/2017 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using Gemstone.Data; + +namespace openXDA.Model +{ + internal class LazyContext + { + #region [ Members ] + + // Fields + private Dictionary m_locations; + private Dictionary m_meters; + private Dictionary m_assetLocations; + private Dictionary m_sourceImpedances; + private Dictionary m_meterAssets; + private Dictionary m_channels; + private Dictionary m_series; + private Dictionary m_measurementTypes; + private Dictionary m_measurementCharacteristics; + private Dictionary m_phases; + private Dictionary m_seriesTypes; + + private Dictionary m_assets; + private Dictionary m_assetConnections; + + #endregion + + #region [ Constructors ] + + public LazyContext() + { + m_locations = new Dictionary(); + m_meters = new Dictionary(); + m_assets = new Dictionary(); + m_assetLocations = new Dictionary(); + m_sourceImpedances = new Dictionary(); + m_meterAssets = new Dictionary(); + m_channels = new Dictionary(); + m_series = new Dictionary(); + m_measurementTypes = new Dictionary(); + m_measurementCharacteristics = new Dictionary(); + m_phases = new Dictionary(); + m_seriesTypes = new Dictionary(); + m_assetConnections = new Dictionary(); + } + + #endregion + + #region [ Properties ] + + public Func ConnectionFactory { get; set; } + + #endregion + + #region [ Methods ] + + public Location GetLocation(int locationID) => + m_locations.TryGetValue(locationID, out Location location) + ? location + : null; + + public Location GetLocation(Location location) + { + Location cachedLocation; + + if ((object)location == null) + return null; + + if (location.ID == 0) + return location; + + if (m_locations.TryGetValue(location.ID, out cachedLocation)) + return cachedLocation; + + m_locations.Add(location.ID, location); + return location; + } + + public Meter GetMeter(int meterID) => + m_meters.TryGetValue(meterID, out Meter meter) + ? meter + : null; + + public Meter GetMeter(Meter meter) + { + Meter cachedMeter; + + if ((object)meter == null) + return null; + + if (meter.ID == 0) + return meter; + + if (m_meters.TryGetValue(meter.ID, out cachedMeter)) + return cachedMeter; + + m_meters.Add(meter.ID, meter); + return meter; + } + + public Asset GetAsset(int assetID) => + m_assets.TryGetValue(assetID, out Asset asset) + ? asset + : null; + + public Asset GetAsset(Asset asset) + { + Asset cachedAsset; + + if ((object)asset == null) + return null; + + if (asset.ID == 0) + return asset; + + if (m_assets.TryGetValue(asset.ID, out cachedAsset)) + return cachedAsset; + + m_assets.Add(asset.ID, asset); + return asset; + } + + public AssetLocation GetAssetLocation(int assetLocationID) => + m_assetLocations.TryGetValue(assetLocationID, out AssetLocation assetLocation) + ? assetLocation + : null; + + public AssetLocation GetAssetLocation(AssetLocation assetLocation) + { + AssetLocation cachedAssetLocation; + + if ((object)assetLocation == null) + return null; + + if (assetLocation.ID == 0) + return assetLocation; + + if (m_assetLocations.TryGetValue(assetLocation.ID, out cachedAssetLocation)) + return cachedAssetLocation; + + m_assetLocations.Add(assetLocation.ID, assetLocation); + return assetLocation; + } + + public SourceImpedance GetSourceImpedance(int sourceImpedanceID) => + m_sourceImpedances.TryGetValue(sourceImpedanceID, out SourceImpedance sourceImpedance) + ? sourceImpedance + : null; + + public SourceImpedance GetSourceImpedance(SourceImpedance sourceImpedance) + { + SourceImpedance cachedSourceImpedance; + + if ((object)sourceImpedance == null) + return null; + + if (sourceImpedance.ID == 0) + return sourceImpedance; + + if (m_sourceImpedances.TryGetValue(sourceImpedance.ID, out cachedSourceImpedance)) + return cachedSourceImpedance; + + m_sourceImpedances.Add(sourceImpedance.ID, sourceImpedance); + return sourceImpedance; + } + + public MeterAsset GetMeterAsset(int meterAssetID) => + m_meterAssets.TryGetValue(meterAssetID, out MeterAsset meterAsset) + ? meterAsset + : null; + + public MeterAsset GetMeterAsset(MeterAsset meterAsset) + { + MeterAsset cachedMeterAsset; + + if ((object)meterAsset == null) + return null; + + if (meterAsset.ID == 0) + return meterAsset; + + if (m_meterAssets.TryGetValue(meterAsset.ID, out cachedMeterAsset)) + return cachedMeterAsset; + + m_meterAssets.Add(meterAsset.ID, meterAsset); + return meterAsset; + } + + public Channel GetChannel(int channelID) => + m_channels.TryGetValue(channelID, out Channel channel) + ? channel + : null; + + public Channel GetChannel(Channel channel) + { + Channel cachedChannelLine; + + if ((object)channel == null) + return null; + + if (channel.ID == 0) + return channel; + + if (m_channels.TryGetValue(channel.ID, out cachedChannelLine)) + return cachedChannelLine; + + m_channels.Add(channel.ID, channel); + return channel; + } + + public Series GetSeries(int seriesID) => + m_series.TryGetValue(seriesID, out Series series) + ? series + : null; + + public Series GetSeries(Series series) + { + Series cachedSeriesLine; + + if ((object)series == null) + return null; + + if (series.ID == 0) + return series; + + if (m_series.TryGetValue(series.ID, out cachedSeriesLine)) + return cachedSeriesLine; + + m_series.Add(series.ID, series); + return series; + } + + public MeasurementType GetMeasurementType(int measurementTypeID) => + m_measurementTypes.TryGetValue(measurementTypeID, out MeasurementType measurementType) + ? measurementType + : null; + + public MeasurementType GetMeasurementType(MeasurementType measurementType) + { + MeasurementType cachedMeasurementTypeLine; + + if ((object)measurementType == null) + return null; + + if (measurementType.ID == 0) + return measurementType; + + if (m_measurementTypes.TryGetValue(measurementType.ID, out cachedMeasurementTypeLine)) + return cachedMeasurementTypeLine; + + m_measurementTypes.Add(measurementType.ID, measurementType); + return measurementType; + } + + public MeasurementCharacteristic GetMeasurementCharacteristic(int measurementCharacteristicID) => + m_measurementCharacteristics.TryGetValue(measurementCharacteristicID, out MeasurementCharacteristic measurementCharacteristic) + ? measurementCharacteristic + : null; + + public MeasurementCharacteristic GetMeasurementCharacteristic(MeasurementCharacteristic measurementCharacteristic) + { + MeasurementCharacteristic cachedMeasurementCharacteristicLine; + + if ((object)measurementCharacteristic == null) + return null; + + if (measurementCharacteristic.ID == 0) + return measurementCharacteristic; + + if (m_measurementCharacteristics.TryGetValue(measurementCharacteristic.ID, out cachedMeasurementCharacteristicLine)) + return cachedMeasurementCharacteristicLine; + + m_measurementCharacteristics.Add(measurementCharacteristic.ID, measurementCharacteristic); + return measurementCharacteristic; + } + + public Phase GetPhase(int phaseID) => + m_phases.TryGetValue(phaseID, out Phase phase) + ? phase + : null; + + public Phase GetPhase(Phase phase) + { + Phase cachedPhaseLine; + + if ((object)phase == null) + return null; + + if (phase.ID == 0) + return phase; + + if (m_phases.TryGetValue(phase.ID, out cachedPhaseLine)) + return cachedPhaseLine; + + m_phases.Add(phase.ID, phase); + return phase; + } + + public SeriesType GetSeriesType(int seriesTypeID) => + m_seriesTypes.TryGetValue(seriesTypeID, out SeriesType seriesType) + ? seriesType + : null; + + public SeriesType GetSeriesType(SeriesType seriesType) + { + SeriesType cachedSeriesTypeLine; + + if ((object)seriesType == null) + return null; + + if (seriesType.ID == 0) + return seriesType; + + if (m_seriesTypes.TryGetValue(seriesType.ID, out cachedSeriesTypeLine)) + return cachedSeriesTypeLine; + + m_seriesTypes.Add(seriesType.ID, seriesType); + return seriesType; + } + + public AssetConnection GetAssetConnection(int connectionID) => + m_assetConnections.TryGetValue(connectionID, out AssetConnection connection) + ? connection + : null; + + public AssetConnection GetAssetConnection(AssetConnection connection) + { + AssetConnection cachedConnection; + + if ((object)connection == null) + return null; + + if (connection.ID == 0) + return connection; + + if (m_assetConnections.TryGetValue(connection.ID, out cachedConnection)) + return cachedConnection; + + m_assetConnections.Add(connection.ID, connection); + return connection; + } + + #endregion + } +} diff --git a/src/Libraries/openXDA.Model/Links/AssetConnection.cs b/src/Libraries/openXDA.Model/Links/AssetConnection.cs new file mode 100644 index 00000000..dc03ab97 --- /dev/null +++ b/src/Libraries/openXDA.Model/Links/AssetConnection.cs @@ -0,0 +1,176 @@ +//****************************************************************************************************** +// AssetConnection.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 12/13/2019 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data; +using Gemstone.Data.Model; +using Newtonsoft.Json; + +namespace openXDA.Model +{ + [TableName("AssetRelationship")] + [PostRoles("Administrator, Transmission SME")] + [PatchRoles("Administrator, Transmission SME")] + [DeleteRoles("Administrator, Transmission SME")] + public class AssetConnection + { + #region [ Members ] + + // Fields + private Asset m_parent; + private Asset m_child; + + #endregion + + #region [ Properties ] + + [PrimaryKey(true)] + public int ID { get; set; } + + public int AssetRelationshipTypeID { get; set; } + + public int ParentID { get; set; } + + public int ChildID { get; set; } + + [JsonIgnore] + [NonRecordField] + public Asset Parent + { + get + { + if (m_parent is null) + m_parent = LazyContext.GetAsset(ParentID); + + if (m_parent is null) + m_parent = QueryParent(); + + return m_parent; + } + set => m_parent = value; + } + + [JsonIgnore] + [NonRecordField] + public Asset Child + { + get + { + if (m_child is null) + m_child = LazyContext.GetAsset(ChildID); + + if (m_child is null) + m_child = QueryChild(); + + return m_child; + } + set => m_child = value; + } + + [JsonIgnore] + [NonRecordField] + public Func ConnectionFactory + { + get => LazyContext.ConnectionFactory; + set => LazyContext.ConnectionFactory = value; + } + + [JsonIgnore] + [NonRecordField] + internal LazyContext LazyContext { get; set; } = new LazyContext(); + + #endregion + + #region [ Methods ] + + public Asset GetParent(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations assetTable = new TableOperations(connection); + Asset parent = assetTable.QueryRecordWhere("ID = {0}", ParentID); + return parent; + } + + public Asset GetChild(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations assetTable = new TableOperations(connection); + Asset child = assetTable.QueryRecordWhere("ID = {0}", ChildID); + + return child; + } + + public Asset QueryParent() + { + Asset parent; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + parent = GetParent(connection); + } + + if ((object)parent != null) + parent.LazyContext = LazyContext; + + return LazyContext.GetAsset(parent); + } + + public Asset QueryChild() + { + Asset child; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + child = GetChild(connection); + } + + if ((object)child != null) + child.LazyContext = LazyContext; + + return LazyContext.GetAsset(child); + } + + #endregion + } + + public class AssetConnectionDetail + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int AssetRelationshipTypeID { get; set; } + + public int ParentID { get; set; } + + public int ChildID { get; set; } + + public string ChildKey { get; set; } + + public string ParentKey { get; set; } + + public string AssetRelationshipType { get; set; } + } +} diff --git a/src/Libraries/openXDA.Model/Links/MeterLine.cs b/src/Libraries/openXDA.Model/Links/MeterLine.cs new file mode 100644 index 00000000..4d44e704 --- /dev/null +++ b/src/Libraries/openXDA.Model/Links/MeterLine.cs @@ -0,0 +1,174 @@ +//****************************************************************************************************** +// MeterLine.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +// 12/13/2019 - C. Lackner +// Modified to fit new Asset Model Structure. +// +//****************************************************************************************************** + +using System; +using System.ComponentModel.DataAnnotations; +using Gemstone.Data; +using Gemstone.Data.Model; +using Newtonsoft.Json; + +namespace openXDA.Model +{ + public class MeterAsset + { + #region [ Members ] + + // Fields + private Meter m_meter; + private Asset m_asset; + + #endregion + + #region [ Properties ] + + [PrimaryKey(true)] + public int ID { get; set; } + + public int MeterID { get; set; } + + public int AssetID { get; set; } + + [JsonIgnore] + [NonRecordField] + public Meter Meter + { + get + { + if (m_meter is null) + m_meter = LazyContext.GetMeter(MeterID); + + if (m_meter is null) + m_meter = QueryMeter(); + + return m_meter; + } + set => m_meter = value; + } + + [JsonIgnore] + [NonRecordField] + public Asset Asset + { + get + { + if (m_asset is null) + m_asset = LazyContext.GetAsset(AssetID); + + if (m_asset is null) + m_asset = QueryAsset(); + + return m_asset; + } + set => m_asset = value; + } + + [JsonIgnore] + [NonRecordField] + public Func ConnectionFactory + { + get => LazyContext.ConnectionFactory; + set => LazyContext.ConnectionFactory = value; + } + + [JsonIgnore] + [NonRecordField] + internal LazyContext LazyContext { get; set; } = new LazyContext(); + + #endregion + + #region [ Methods ] + + public Meter GetMeter(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations meterTable = new TableOperations(connection); + return meterTable.QueryRecordWhere("ID = {0}", MeterID); + } + + public Asset GetAsset(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations assetTable = new TableOperations(connection); + return assetTable.QueryRecordWhere("ID = {0}", AssetID); + } + + public Meter QueryMeter() + { + Meter meter; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + meter = GetMeter(connection); + } + + if ((object)meter != null) + meter.LazyContext = LazyContext; + + return LazyContext.GetMeter(meter); + } + + public Asset QueryAsset() + { + Asset asset; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + asset = GetAsset(connection); + } + + if ((object)asset != null) + asset.LazyContext = LazyContext; + + return LazyContext.GetAsset(asset); + } + + #endregion + } + + public class MeterAssetDetail + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int MeterID { get; set; } + + public int AssetID { get; set; } + + public string MeterKey { get; set; } + + public string AssetKey { get; set; } + + public string AssetName { get; set; } + + public string AssetType { get; set; } + + public string FaultDetectionLogic { get; set; } + } +} diff --git a/src/Libraries/openXDA.Model/Links/MeterLocationLine.cs b/src/Libraries/openXDA.Model/Links/MeterLocationLine.cs new file mode 100644 index 00000000..377ec143 --- /dev/null +++ b/src/Libraries/openXDA.Model/Links/MeterLocationLine.cs @@ -0,0 +1,192 @@ +//****************************************************************************************************** +// MeterLocationLine.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 06/19/2017 - Billy Ernest +// Generated original version of source code. +// 12/13/2019 - C. Lackner +// Update to reflect changes in Location and move from Line to Asset. +// +//****************************************************************************************************** + +using Gemstone.Data; +using Gemstone.Data.Model; +using Newtonsoft.Json; + +namespace openXDA.Model +{ + public class AssetLocation + { + #region [ Members ] + + // Fields + private Location m_location; + private Asset m_asset; + private SourceImpedance m_sourceImpedance; + + #endregion + + #region [ Properties ] + + [PrimaryKey(true)] + public int ID { get; set; } + + public int LocationID { get; set; } + + public int AssetID { get; set; } + + [JsonIgnore] + [NonRecordField] + public Location Location + { + get + { + if (m_location is null) + m_location = LazyContext.GetLocation(LocationID); + + if (m_location is null) + m_location = QueryLocation(); + + return m_location; + } + set => m_location = value; + } + + [JsonIgnore] + [NonRecordField] + public Asset Asset + { + get + { + if (m_asset is null) + m_asset = LazyContext.GetAsset(AssetID); + + if (m_asset is null) + m_asset = QueryAsset(); + + return m_asset; + } + set => m_asset = value; + } + + [JsonIgnore] + [NonRecordField] + public SourceImpedance SourceImpedance + { + get => m_sourceImpedance ?? (m_sourceImpedance ?? QuerySourceImpedance()); + set => m_sourceImpedance = value; + } + + [JsonIgnore] + [NonRecordField] + public Func ConnectionFactory + { + get => LazyContext.ConnectionFactory; + set => LazyContext.ConnectionFactory = value; + } + + [JsonIgnore] + [NonRecordField] + internal LazyContext LazyContext { get; set; } = new LazyContext(); + + #endregion + + #region [ Methods ] + + public Location GetLocation(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations locationTable = new TableOperations(connection); + + try + { + return locationTable.QueryRecordWhere("ID = {0}", LocationID); + } + catch + { + return null; + } + } + + public Asset GetAsset(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations assetTable = new TableOperations(connection); + return assetTable.QueryRecordWhere("ID = {0}", AssetID); + } + + public SourceImpedance GetSourceImpedance(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations sourceImpedanceTable = new TableOperations(connection); + return sourceImpedanceTable.QueryRecordWhere("AssetLocationID = {0}", ID); + } + + private Location QueryLocation() + { + Location location; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + location = GetLocation(connection); + } + + if ((object)location != null) + location.LazyContext = LazyContext; + + return LazyContext.GetLocation(location); + } + + private Asset QueryAsset() + { + Asset asset; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + asset = GetAsset(connection); + } + + if ((object)asset != null) + asset.LazyContext = LazyContext; + + return LazyContext.GetAsset(asset); + } + + private SourceImpedance QuerySourceImpedance() + { + SourceImpedance sourceImpedance; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + sourceImpedance = GetSourceImpedance(connection); + } + + if ((object)sourceImpedance != null) + sourceImpedance.LazyContext = LazyContext; + + return LazyContext.GetSourceImpedance(sourceImpedance); + } + + #endregion + } +} diff --git a/src/Libraries/openXDA.Model/Meters/Location.cs b/src/Libraries/openXDA.Model/Meters/Location.cs new file mode 100644 index 00000000..6c8515d1 --- /dev/null +++ b/src/Libraries/openXDA.Model/Meters/Location.cs @@ -0,0 +1,509 @@ +//****************************************************************************************************** +// Location.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// 12/13/2019 - Christoph Lackner +// Updated MeterLocation to more Generic Location. +// +//****************************************************************************************************** + +using System.ComponentModel.DataAnnotations; +using System.Data; +using System.Text.RegularExpressions; +using Gemstone.Collections.CollectionExtensions; +using Gemstone.Data; +using Gemstone.Data.DataExtensions; +using Gemstone.Data.Model; +using Newtonsoft.Json; + +namespace openXDA.Model +{ + [PostRoles("Administrator, Transmission SME")] + [DeleteRoles("Administrator, Transmission SME")] + [PatchRoles("Administrator, Transmission SME")] + public class Location + { + #region [ Members ] + + // Nested Types + private delegate void ChannelConnector(int assetID, IEnumerable channels); + private delegate IEnumerable AssetConnectionLookup(int assetID); + + private class AssetConnectionDetail + { + #region [ Constructors ] + + public AssetConnectionDetail(int parentID, int childID, string jumpSQL, string passthroughSQL) + { + ParentID = parentID; + ChildID = childID; + JumpSQL = jumpSQL; + PassthroughSQL = passthroughSQL; + } + + #endregion + + #region [ Properties ] + + public int ParentID { get; } + public int ChildID { get; } + private string JumpSQL { get; } + private string PassthroughSQL { get; } + + private HashSet JumpChannels { get; } = new HashSet(); + private HashSet PassthroughChannels { get; } = new HashSet(); + private bool Populated { get; set; } + + #endregion + + #region [ Methods ] + + public void PopulateChannelSets(AdoDataConnection connection, int locationID, Func channelLookup) + { + if (Populated) + return; + + // lang=regex + const string Pattern = @"\{(?:parentid|childid|channelid)\}"; + string jumpSQL = Regex.Replace(JumpSQL, Pattern, ReplaceFormatParameter, RegexOptions.IgnoreCase); + string passthroughSQL = Regex.Replace(PassthroughSQL, Pattern, ReplaceFormatParameter, RegexOptions.IgnoreCase); + + string queryFormat = + $"SELECT " + + $" SourceChannel.ID ChannelID, " + + $" Jump.Value Jump, " + + $" Passthrough.Value Passthrough " + + $"FROM " + + $" Asset ParentAsset JOIN " + + $" Asset ChildAsset ON " + + $" ParentAsset.ID = {{0}} AND " + + $" ChildAsset.ID = {{1}} JOIN " + + $" Location ON Location.ID = {{2}} JOIN " + + $" Meter ON Meter.LocationID = Location.ID JOIN " + + $" Channel SourceChannel ON SourceChannel.MeterID = Meter.ID CROSS APPLY " + + $" ({jumpSQL}) Jump(Value) CROSS APPLY " + + $" ({passthroughSQL}) Passthrough(Value) " + + $"WHERE " + + $" Jump.Value <> 0 OR " + + $" Passthrough.Value <> 0"; + + using (DataTable table = connection.RetrieveData(queryFormat, ParentID, ChildID, locationID)) + { + foreach (DataRow row in table.AsEnumerable()) + { + int channelID = row.ConvertField("ChannelID"); + bool jump = row.ConvertField("Jump"); + bool passthrough = row.ConvertField("Passthrough"); + Channel channel = channelLookup(channelID); + if (channel is null) continue; + if (jump) JumpChannels.Add(channel); + if (passthrough) PassthroughChannels.Add(channel); + } + } + + Populated = true; + + string ReplaceFormatParameter(Match match) + { + switch (match.Value.ToLowerInvariant()) + { + case "{parentid}": return "ParentAsset.ID"; + case "{childid}": return "ChildAsset.ID"; + case "{channelid}": return "SourceChannel.ID"; + default: return match.Value; + } + } + } + + public bool CanJump(Channel channel) => + JumpChannels.Contains(channel); + + public bool CanPassThrough(Channel channel) => + PassthroughChannels.Contains(channel); + + #endregion + } + + private class TraversalContext + { + public AdoDataConnection Connection { get; } + public HashSet VisitedAssets { get; } + public AssetConnectionLookup FindAssetConnections { get; } + public ChannelConnector ConnectChannels { get; } + + public TraversalContext(AdoDataConnection connection, HashSet visitedAssets, AssetConnectionLookup findAssetConnections, ChannelConnector connectChannels) + { + Connection = connection; + VisitedAssets = visitedAssets; + FindAssetConnections = findAssetConnections; + ConnectChannels = connectChannels; + } + } + + // Fields + private List m_meters; + private List m_assetLocations; + + #endregion + + #region [ Properties ] + + [PrimaryKey(true)] + public int ID { get; set; } + + [StringLength(50)] + [Required] + [DefaultSortOrder] + public string LocationKey { get; set; } + + [StringLength(200)] + [Required] + public string Name { get; set; } + + [StringLength(200)] + public string Alias { get; set; } + + [StringLength(50)] + public string ShortName { get; set; } + + [Required] + public double Latitude { get; set; } + + [Required] + public double Longitude { get; set; } + + [StringLength(200)] + public string Description { get; set; } + + [JsonIgnore] + [NonRecordField] + public List Meters + { + get + { + return m_meters ?? (m_meters = QueryMeters()); + } + set + { + m_meters = value; + } + } + + [JsonIgnore] + [NonRecordField] + public List AssetLocations + { + get + { + return m_assetLocations ?? (m_assetLocations = QueryAssetLocations()); + } + set + { + m_assetLocations = value; + } + } + + [JsonIgnore] + [NonRecordField] + public Func ConnectionFactory + { + get + { + return LazyContext.ConnectionFactory; + } + set + { + LazyContext.ConnectionFactory = value; + } + } + + [JsonIgnore] + [NonRecordField] + internal LazyContext LazyContext { get; set; } = new LazyContext(); + + #endregion + + #region [ Methods ] + + public IEnumerable GetMeters(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations meterTable = new TableOperations(connection); + return meterTable.QueryRecordsWhere("MeterLocationID = {0}", ID); + } + + public IEnumerable GetAssetLocations(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations assetLocationTable = new TableOperations(connection); + return assetLocationTable.QueryRecordsWhere("LocationID = {0}", ID); + } + + private List QueryMeters() + { + List meters; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + meters = GetMeters(connection)? + .Select(LazyContext.GetMeter) + .ToList(); + } + + if ((object)meters != null) + { + foreach (Meter meter in meters) + { + meter.Location = this; + meter.LazyContext = LazyContext; + } + } + + return meters; + } + + private List QueryAssetLocations() + { + List assetLocations; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + assetLocations = GetAssetLocations(connection)? + .Select(LazyContext.GetAssetLocation) + .ToList(); + } + + if ((object)assetLocations != null) + { + foreach (AssetLocation assetLocation in assetLocations) + { + assetLocation.Location = this; + assetLocation.LazyContext = LazyContext; + } + } + + return assetLocations; + } + + public void ConnectAllChannels() + { + if (ConnectionFactory is null) + return; + + Dictionary> connectedChannelLookup = new Dictionary>(); + ChannelConnector connectChannels = CreateChannelConnector(connectedChannelLookup); + + using (AdoDataConnection connection = ConnectionFactory()) + { + TraverseAssetConnections(connection, connectChannels); + + TableOperations assetTable = new TableOperations(connection); + + foreach (KeyValuePair> kvp in connectedChannelLookup) + { + int assetID = kvp.Key; + HashSet connectedChannels = kvp.Value; + Asset asset = assetTable.QueryRecordWhere("ID = {0}", assetID); + asset = LazyContext.GetAsset(asset); + asset.ConnectedChannels = connectedChannels.ToList(); + asset.LazyContext = LazyContext; + } + } + + // Assign empty lists to any assets that were missed by the recursive search + foreach (Asset asset in AssetLocations.Select(al => al.Asset)) + EnsureConnectedChannels(asset); + } + + private void TraverseAssetConnections(AdoDataConnection connection, ChannelConnector connectChannels) + { + List allChannels = RetrieveAllChannels(connection).ToList(); + List allAssetConnections = RetrieveAllAssetConnections(connection).ToList(); + AssetConnectionLookup findAssetConnections = CreateAssetConnectionLookup(connection, allChannels, allAssetConnections); + + foreach (IGrouping rootChannels in allChannels.GroupBy(channel => channel.AssetID)) + { + int rootAssetID = rootChannels.Key; + + // Initialize an empty set of connected channels in case the root asset has no connected channels + connectChannels(rootAssetID, Enumerable.Empty()); + + foreach (AssetConnectionDetail assetConnection in findAssetConnections(rootAssetID)) + { + List jumpChannels = rootChannels + .Where(assetConnection.CanJump) + .ToList(); + + if (jumpChannels.Count == 0) + continue; + + int connectedAssetID = assetConnection.ChildID; + connectChannels(connectedAssetID, jumpChannels); + + HashSet visitedAssets = new HashSet() { rootAssetID }; + TraversalContext context = new TraversalContext(connection, visitedAssets, findAssetConnections, connectChannels); + TraverseAssetConnections(context, jumpChannels, connectedAssetID); + } + } + } + + private void TraverseAssetConnections(TraversalContext context, List connectedChannels, int visitedAssetID) + { + AdoDataConnection connection = context.Connection; + HashSet visitedAssets = context.VisitedAssets; + AssetConnectionLookup findAssetConnections = context.FindAssetConnections; + ChannelConnector connectChannels = context.ConnectChannels; + + visitedAssets.Add(visitedAssetID); + + foreach (AssetConnectionDetail assetConnection in findAssetConnections(visitedAssetID)) + { + int connectedAssetID = assetConnection.ChildID; + + if (visitedAssets.Contains(connectedAssetID)) + continue; + + List passthroughChannels = connectedChannels + .Where(assetConnection.CanPassThrough) + .ToList(); + + if (passthroughChannels.Count == 0) + continue; + + connectChannels(connectedAssetID, passthroughChannels); + TraverseAssetConnections(context, passthroughChannels, connectedAssetID); + } + + visitedAssets.Remove(visitedAssetID); + } + + private IEnumerable RetrieveAllChannels(AdoDataConnection connection) + { + const string QueryFormat = + "SELECT Channel.* " + + "FROM " + + " Channel JOIN " + + " Meter ON Channel.MeterID = Meter.ID " + + "WHERE Meter.LocationID = {0}"; + + TableOperations channelTable = new TableOperations(connection); + + using (DataTable table = connection.RetrieveData(QueryFormat, ID)) + { + foreach (DataRow row in table.AsEnumerable()) + { + Channel channel = channelTable.LoadRecord(row); + channel = LazyContext.GetChannel(channel); + yield return channel; + } + } + } + + private IEnumerable RetrieveAllAssetConnections(AdoDataConnection connection) + { + const string QueryFormat = + "SELECT " + + " AssetConnection.ParentID, " + + " AssetConnection.ChildID, " + + " AssetRelationshipType.JumpConnection JumpSQL, " + + " AssetRelationshipType.PassThrough PassthroughSQL " + + "FROM " + + " Location JOIN " + + " AssetConnection ON Location.ID = {0} JOIN " + + " AssetRelationshipType ON AssetConnection.AssetRelationshipTypeID = AssetRelationshipType.ID JOIN " + + " AssetLocation ParentLocation ON " + + " ParentLocation.LocationID = Location.ID AND " + + " ParentLocation.AssetID = AssetConnection.ParentID JOIN " + + " AssetLocation ChildLocation ON " + + " ChildLocation.LocationID = Location.ID AND " + + " ChildLocation.AssetID = AssetConnection.ChildID"; + + using (DataTable table = connection.RetrieveData(QueryFormat, ID)) + { + foreach (DataRow row in table.AsEnumerable()) + { + int parentID = row.ConvertField("ParentID"); + int childID = row.ConvertField("ChildID"); + string jumpSQL = row.ConvertField("JumpSQL"); + string passthroughSQL = row.ConvertField("PassthroughSQL"); + yield return new AssetConnectionDetail(parentID, childID, jumpSQL, passthroughSQL); + yield return new AssetConnectionDetail(childID, parentID, jumpSQL, passthroughSQL); + } + } + } + + private AssetConnectionLookup CreateAssetConnectionLookup(AdoDataConnection connection, List allChannels, List allAssetConnections) + { + Dictionary channelLookup = allChannels.ToDictionary(channel => channel.ID); + ILookup assetConnectionLookup = allAssetConnections.ToLookup(conn => conn.ParentID); + return findAssetConnection; + + Channel findChannel(int channelID) => + channelLookup.TryGetValue(channelID, out Channel channel) + ? channel + : null; + + IEnumerable findAssetConnection(int assetID) + { + foreach (AssetConnectionDetail assetConnection in assetConnectionLookup[assetID]) + { + assetConnection.PopulateChannelSets(connection, ID, findChannel); + yield return assetConnection; + } + } + } + + #endregion + + #region [ Static ] + + // Static Methods + private static ChannelConnector CreateChannelConnector(Dictionary> connectedChannelLookup) + { + return (assetID, channels) => + { + HashSet connectedChannels = connectedChannelLookup.GetOrAdd(assetID, _ => new HashSet()); + connectedChannels.UnionWith(channels); + }; + } + + private static void EnsureConnectedChannels(Asset asset) + { + Func connectionFactory = asset.ConnectionFactory; + + try + { + asset.ConnectionFactory = null; + + if (asset.ConnectedChannels is null) + asset.ConnectedChannels = new List(); + } + finally + { + asset.ConnectionFactory = connectionFactory; + } + } + + #endregion + } +} diff --git a/src/Libraries/openXDA.Model/Meters/Meter.cs b/src/Libraries/openXDA.Model/Meters/Meter.cs new file mode 100644 index 00000000..47c6b517 --- /dev/null +++ b/src/Libraries/openXDA.Model/Meters/Meter.cs @@ -0,0 +1,338 @@ +//****************************************************************************************************** +// Meter.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +// 12/13/2019 - C. Lackner +// Updated to fit in new Asset based model structure. +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Gemstone.ComponentModel.DataAnnotations; +using Gemstone.Data; +using Gemstone.Data.Model; +using Newtonsoft.Json; + +namespace openXDA.Model +{ + [PostRoles("Administrator, Transmission SME")] + [DeleteRoles("Administrator, Transmission SME")] + [PatchRoles("Administrator, Transmission SME")] + public class Meter + { + #region [ Members ] + + // Fields + private Location m_location; + private List m_meterAssets; + private List m_channels; + + #endregion + + #region [ Properties ] + + [PrimaryKey(true)] + public int ID { get; set; } + + [Required] + [StringLength(50)] + [DefaultSortOrder] + public string AssetKey { get; set; } + + [Required] + [Label("Location")] + public int LocationID { get; set; } + + [Required] + [StringLength(200)] + public string Name { get; set; } + + [StringLength(200)] + public string Alias { get; set; } + + [StringLength(12)] + public string ShortName { get; set; } + + [Required] + [StringLength(200)] + public string Make { get; set; } + + [Required] + [StringLength(200)] + public string Model { get; set; } + + + [StringLength(200)] + public string TimeZone { get; set; } + + public string Description { get; set; } + + [JsonIgnore] + [NonRecordField] + public Location Location + { + get + { + if (m_location is null) + m_location = LazyContext.GetLocation(LocationID); + + if (m_location is null) + m_location = QueryLocation(); + + return m_location; + } + set => m_location = value; + } + + [JsonIgnore] + [NonRecordField] + public List MeterAssets + { + get => m_meterAssets ?? (m_meterAssets = QueryMeterAssets()); + set => m_meterAssets = value; + } + + [JsonIgnore] + [NonRecordField] + public List Channels + { + get => m_channels ?? (m_channels = QueryChannels()); + set => m_channels = value; + } + + public List Series + { + get + { + List channels = Channels; + + if (channels is null) + return null; + + bool IsQueryRequired() + { + var connectionFactory = ConnectionFactory; + + try + { + // Don't trigger individual queries for each channel + ConnectionFactory = null; + return channels.Any(channel => channel.Series is null); + } + finally + { + ConnectionFactory = connectionFactory; + } + } + + if (IsQueryRequired()) + return QuerySeries(); + + return channels + .SelectMany(channel => channel.Series) + .ToList(); + } + } + + [JsonIgnore] + [NonRecordField] + public Func ConnectionFactory + { + get => LazyContext.ConnectionFactory; + set => LazyContext.ConnectionFactory = value; + } + + [JsonIgnore] + [NonRecordField] + internal LazyContext LazyContext { get; set; } = new LazyContext(); + + #endregion + + #region [ Methods ] + + public Location GetLocation(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations locationTable = new TableOperations(connection); + return locationTable.QueryRecordWhere("ID = {0}", LocationID); + } + + public IEnumerable GetMeterAssets(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations meterAssetTable = new TableOperations(connection); + return meterAssetTable.QueryRecordsWhere("MeterID = {0}", ID); + } + + public IEnumerable GetChannels(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations channelTable = new TableOperations(connection); + return channelTable.QueryRecordsWhere("MeterID = {0}", ID); + } + + public IEnumerable GetSeries(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations seriesTable = new TableOperations(connection); + return seriesTable.QueryRecordsWhere("ChannelID IN (SELECT ID FROM Channel WHERE MeterID = {0})", ID); + } + + public TimeZoneInfo GetTimeZoneInfo(TimeZoneInfo defaultTimeZone) + { + if (!string.IsNullOrEmpty(TimeZone)) + return TimeZoneInfo.FindSystemTimeZoneById(TimeZone); + + return defaultTimeZone; + } + + private Location QueryLocation() + { + Location location; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + location = GetLocation(connection); + } + + if ((object)location != null) + location.LazyContext = LazyContext; + + return LazyContext.GetLocation(location); + } + + private List QueryMeterAssets() + { + List meterAssets; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + meterAssets = GetMeterAssets(connection)? + .Select(LazyContext.GetMeterAsset) + .ToList(); + } + + if ((object)meterAssets != null) + { + foreach (MeterAsset meterAsset in meterAssets) + { + meterAsset.Meter = this; + meterAsset.LazyContext = LazyContext; + } + } + + return meterAssets; + } + + private List QueryChannels() + { + List channels; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + channels = GetChannels(connection)? + .Select(LazyContext.GetChannel) + .ToList(); + } + + if ((object)channels != null) + { + foreach (Channel channel in channels) + { + channel.Meter = this; + channel.LazyContext = LazyContext; + } + } + + return channels; + } + + private List QuerySeries() + { + List seriesList; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + seriesList = GetSeries(connection)? + .Select(LazyContext.GetSeries) + .ToList(); + } + + if (!(seriesList is null)) + { + ILookup seriesLookup = seriesList.ToLookup(series => series.ChannelID); + + foreach (Channel channel in Channels) + { + channel.Series = seriesLookup[channel.ID].ToList(); + + foreach (Series series in channel.Series) + { + series.Channel = channel; + series.LazyContext = LazyContext; + } + } + } + + return seriesList; + } + + #endregion + } + + public class MeterDetail : Meter + { + public new string Location { get; set; } + + public string TimeZoneLabel + { + get + { + try + { + if (TimeZone != "UTC") + return TimeZoneInfo.FindSystemTimeZoneById(TimeZone).ToString(); + } + catch + { + // Do not fail if the time zone cannot be found -- + // instead, fall through to the logic below to + // find the label for UTC + } + + return TimeZoneInfo.GetSystemTimeZones() + .Where(info => info.Id == "UTC") + .DefaultIfEmpty(TimeZoneInfo.Utc) + .First() + .ToString(); + } + } + } +} diff --git a/src/OpenSEE/App_Start/BundleConfig.cs b/src/Libraries/openXDA.Model/PQDigest/HomeScreenWidget.cs similarity index 71% rename from src/OpenSEE/App_Start/BundleConfig.cs rename to src/Libraries/openXDA.Model/PQDigest/HomeScreenWidget.cs index 4716bfc5..ee826388 100644 --- a/src/OpenSEE/App_Start/BundleConfig.cs +++ b/src/Libraries/openXDA.Model/PQDigest/HomeScreenWidget.cs @@ -1,5 +1,5 @@ //****************************************************************************************************** -// BundleConfig.cs - Gbtc +// HomeScreenWidget.cs - Gbtc // // Copyright © 2020, Grid Protection Alliance. All Rights Reserved. // @@ -16,26 +16,29 @@ // // Code Modification History: // ---------------------------------------------------------------------------------------------------- -// 02/19/2020 - Billy Ernest +// 08/10/2020 - C. Lackner // Generated original version of source code. // //****************************************************************************************************** -using System.Web; -using System.Web.Optimization; +using Gemstone.Data.Model; -namespace OpenSEE +namespace PQDigest.Model { - public class BundleConfig + /// + /// Defines a widget used in PQDigest Home Screen + /// + [TableName("PQDigest.HomeScreenWidget"), UseEscapedName] + [PostRoles("Administrator")] + [DeleteRoles("Administrator")] + [PatchRoles("Administrator")] + public class HomeScreenWidget : Widget { - // For more information on bundling, visit https://go.microsoft.com/fwlink/?LinkId=301862 - public static void RegisterBundles(BundleCollection bundles) - { - // Code removed for clarity. -#if Debug - BundleTable.EnableOptimizations = true; -#endif + #region [ Properties ] - } + public int TimeFrame { get; set; } + + #endregion } -} + +} \ No newline at end of file diff --git a/src/OpenSEE/GlobalSettings.cs b/src/Libraries/openXDA.Model/PQDigest/Widget.cs similarity index 54% rename from src/OpenSEE/GlobalSettings.cs rename to src/Libraries/openXDA.Model/PQDigest/Widget.cs index 6e9401a0..6042dea0 100644 --- a/src/OpenSEE/GlobalSettings.cs +++ b/src/Libraries/openXDA.Model/PQDigest/Widget.cs @@ -1,5 +1,5 @@ //****************************************************************************************************** -// GlobalSettings.cs - Gbtc +// Widget.cs - Gbtc // // Copyright © 2020, Grid Protection Alliance. All Rights Reserved. // @@ -16,31 +16,36 @@ // // Code Modification History: // ---------------------------------------------------------------------------------------------------- -// 02/19/2020 - Billy Ernest +// 08/10/2020 - C. Lackner // Generated original version of source code. // //****************************************************************************************************** -using System.Collections.Generic; +using Gemstone.Data.Model; -namespace OpenSEE +namespace PQDigest.Model { - public class GlobalSettings + /// + /// Defines a widget used in PQDigest + /// + [TableName("PQDigest.EventViewWidget"), UseEscapedName] + [PostRoles("Administrator")] + [DeleteRoles("Administrator")] + [PatchRoles("Administrator")] + public class Widget { - public string CompanyName{ get; set; } - public string CompanyAcronym{ get; set; } - public string ApplicationName{ get; set; } - public string ApplicationDescription{ get; set; } - public string ApplicationKeywords{ get; set; } - public string DateFormat{ get; set; } - public string TimeFormat{ get; set; } - public string DateTimeFormat{ get; set; } - public string PasswordRequirementsRegex{ get; set; } - public string PasswordRequirementsError{ get; set; } - public string BootstrapTheme{ get; set; } - - public readonly Dictionary ApplicationSettings = new Dictionary(); - public readonly Dictionary LayoutSettings = new Dictionary(); - public readonly Dictionary PageDefaults = new Dictionary(); + #region [ Properties ] + + [PrimaryKey(true)] + public int ID { get; set; } + + public string Name { get; set; } + + public string Setting { get; set; } + + public string Type { get; set; } + + #endregion } + } \ No newline at end of file diff --git a/src/OpenSEE/Controllers/LoginController.cs b/src/Libraries/openXDA.Model/SEBrowser/Widget.cs similarity index 54% rename from src/OpenSEE/Controllers/LoginController.cs rename to src/Libraries/openXDA.Model/SEBrowser/Widget.cs index 0ddd37aa..dcf2b368 100644 --- a/src/OpenSEE/Controllers/LoginController.cs +++ b/src/Libraries/openXDA.Model/SEBrowser/Widget.cs @@ -1,7 +1,7 @@ //****************************************************************************************************** -// LoginController.cs - Gbtc +// Widget.cs - Gbtc // -// Copyright © 2023, Grid Protection Alliance. All Rights Reserved. +// 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. @@ -16,48 +16,46 @@ // // Code Modification History: // ---------------------------------------------------------------------------------------------------- -// 02/26/2023 - J. Ritchie Carroll +// 08/10/2020 - C. Lackner // Generated original version of source code. // //****************************************************************************************************** -using System; -using System.Threading; -using System.Web.Mvc; -using GSF.Web.Security; +using Gemstone.Data.Model; -namespace OpenSEE.Controllers; - -public class LoginController : Controller +namespace SEBrowser.Model { - [AllowAnonymous] - public ActionResult Index() + /// + /// Defines a widget used in SEBrowser + /// + [TableName("SEBrowser.Widget"), UseEscapedName] + [PostRoles("Administrator")] + [DeleteRoles("Administrator")] + [PatchRoles("Administrator")] + public class Widget { - if (!Startup.OwinLoaded) - throw new InvalidOperationException("Owin pipeline not loaded. Try running 'update-package Microsoft.Owin.Host.SystemWeb -reinstall' from NuGet Package Manager Console."); + #region [ Properties ] - return View(); - } + [PrimaryKey(true)] + public int ID { get; set; } - [Route("~/AuthTest")] - [AuthorizeControllerRole] - public ActionResult AuthTest() - { - return View(); - } + public string Name { get; set; } - [Route("~/Logout")] - [AllowAnonymous] - public ActionResult Logout() - { - return View(); + public string Setting { get; set; } + + public string Type { get; set; } + + #endregion } - [Route("~/UserInfo")] - [AuthorizeControllerRole] - public ActionResult UserInfo() + [TableName("SEbrowser.WidgetView"), UseEscapedName] + [PostRoles("Administrator")] + [DeleteRoles("Administrator")] + [PatchRoles("Administrator")] + public class WidgetView : Widget { - Thread.CurrentPrincipal = ViewBag.SecurityPrincipal = User; - return View(); + [ParentKey(typeof (WidgetCategory))] + public int CategoryID { get; set; } } + } \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/SEBrowser/WidgetCategory.cs b/src/Libraries/openXDA.Model/SEBrowser/WidgetCategory.cs new file mode 100644 index 00000000..62faf93e --- /dev/null +++ b/src/Libraries/openXDA.Model/SEBrowser/WidgetCategory.cs @@ -0,0 +1,52 @@ +//****************************************************************************************************** +// WidgetCategory.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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/10/2020 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace SEBrowser.Model +{ + /// + /// Defines the categories of widgets used in SEBrowser + /// + /// + /// Will need to use in SEbrowser ans OpenSEE too. + /// + [TableName("SEBrowser.WidgetCategory"), UseEscapedName] + [PostRoles("Administrator")] + [DeleteRoles("Administrator")] + [PatchRoles("Administrator")] + public class WidgetCategory + { + #region [ Properties ] + + [PrimaryKey(true)] + public int ID { get; set; } + + public string Name { get; set; } + + [DefaultSortOrder(true)] + public int OrderBy { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/Settings/OpenSEESetting.cs b/src/Libraries/openXDA.Model/Settings/OpenSEESetting.cs new file mode 100644 index 00000000..a1e5adc9 --- /dev/null +++ b/src/Libraries/openXDA.Model/Settings/OpenSEESetting.cs @@ -0,0 +1,33 @@ +//****************************************************************************************************** +// OpenSEESetting.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/05/2026 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [TableName("OpenSEE.Setting"), UseEscapedName] + [PostRoles("Administrator")] + [DeleteRoles("Administrator")] + [PatchRoles("Administrator")] + public class OpenSEESetting : Setting { } +} \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/Settings/PQDigestSetting.cs b/src/Libraries/openXDA.Model/Settings/PQDigestSetting.cs new file mode 100644 index 00000000..877bff81 --- /dev/null +++ b/src/Libraries/openXDA.Model/Settings/PQDigestSetting.cs @@ -0,0 +1,33 @@ +//****************************************************************************************************** +// PQDigestSetting.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 10/20/2025 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [TableName("PQDigest.Setting"), UseEscapedName] + [PostRoles("Administrator")] + [DeleteRoles("Administrator")] + [PatchRoles("Administrator")] + public class PQDigestSetting : Setting { } +} \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/Settings/Setting.cs b/src/Libraries/openXDA.Model/Settings/Setting.cs new file mode 100644 index 00000000..b9f2a866 --- /dev/null +++ b/src/Libraries/openXDA.Model/Settings/Setting.cs @@ -0,0 +1,84 @@ +//****************************************************************************************************** +// Setting.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel.DataAnnotations; +using Gemstone.ComponentModel.DataAnnotations; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [DeleteRoles("Administrator")] + [PatchRoles("Administrator")] + [PostRoles("Administrator")] + [TableName("Setting")] + [UseEscapedName] + public class Setting + { + [PrimaryKey(true)] + public int ID { get; set; } + + public string Name { get; set; } + + public string Value { get; set; } + + public string DefaultValue { get; set; } + } + + [TableName("DashSettings")] + public class DashSettings + { + [PrimaryKey(true)] + public int ID { get; set; } + + [Required] + [StringLength(500)] + public string Name { get; set; } + + [Required] + [StringLength(500)] + public string Value { get; set; } + + [Required] + public bool Enabled { get; set; } + } + + [PrimaryLabel("Name")] + [TableName("UserDashSettings")] + public class UserDashSettings + { + [PrimaryKey(true)] + public int ID { get; set; } + + [Required] + [Label("User Account")] + public Guid UserAccountID { get; set; } + + [Required] + public string Name { get; set; } + + [Required] + public string Value { get; set; } + + public bool Enabled { get; set; } + } +} \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/TransmissionElements/Asset.cs b/src/Libraries/openXDA.Model/TransmissionElements/Asset.cs new file mode 100644 index 00000000..1d484d37 --- /dev/null +++ b/src/Libraries/openXDA.Model/TransmissionElements/Asset.cs @@ -0,0 +1,615 @@ +//****************************************************************************************************** +// Asset.cs - Gbtc +// +// Copyright © 2019, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 12/12/2019 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Data; +using System.Text.RegularExpressions; +using Gemstone.Collections.CollectionExtensions; +using Gemstone.Data; +using Gemstone.Data.DataExtensions; +using Gemstone.Data.Model; +using Gemstone.StringExtensions; +using Newtonsoft.Json; + +namespace openXDA.Model +{ + [PostRoles("Administrator, Transmission SME")] + [DeleteRoles("Administrator, Transmission SME")] + [PatchRoles("Administrator, Transmission SME")] + public class Asset + { + #region [ Members ] + + // Nested Types + private delegate List ConnectedChannelLookup(int parentID, int childID, string traversalSQL); + + private class TraversalContext + { + public AdoDataConnection Connection { get; } + public int LocationID { get; } + + public ConnectedChannelLookup ConnectedChannelLookup { get; } + public HashSet VisitedAssets { get; } + public HashSet ConnectedChannels { get; } + + public TraversalContext(AdoDataConnection connection, int locationID) + { + Connection = connection; + LocationID = locationID; + + ConnectedChannelLookup = GetConnectedChannelLookup(connection, locationID); + VisitedAssets = new HashSet(); + ConnectedChannels = new HashSet(); + } + } + + // Fields + private List m_assetLocations; + private List m_meterAssets; + private List m_directChannels; + private List m_connectedChannels; + private List m_connections; + + #endregion + + #region [ Properties ] + + [PrimaryKey(true)] + public int ID { get; set; } + + [Required] + public double VoltageKV { get; set; } + + [Required] + [StringLength(50)] + [DefaultSortOrder] + public string AssetKey { get; set; } + + public string Description { get; set; } + + [DefaultValue("")] + public string AssetName { get; set; } + + [Required] + public int AssetTypeID {get; set; } + + public bool Spare { get; set; } + + [JsonIgnore] + [NonRecordField] + internal LazyContext LazyContext { get; set; } = new LazyContext(); + + + [JsonIgnore] + [NonRecordField] + public List AssetLocations + { + get + { + return m_assetLocations ?? (m_assetLocations = QueryAssetLocations()); + } + set + { + m_assetLocations = value; + } + } + + [JsonIgnore] + [NonRecordField] + public List MeterAssets + { + get + { + return m_meterAssets ?? (m_meterAssets = QueryMeterAssets()); + } + set + { + m_meterAssets = value; + } + } + + [JsonIgnore] + [NonRecordField] + public List DirectChannels + { + get + { + return m_directChannels ?? (m_directChannels = QueryChannels()); + } + set + { + m_directChannels = value; + } + } + + [JsonIgnore] + [NonRecordField] + public List Connections + { + get + { + return m_connections ?? (m_connections = QueryConnections()); + } + set + { + m_connections = value; + } + } + + [JsonIgnore] + [NonRecordField] + public List ConnectedChannels + { + get + { + return m_connectedChannels ?? (m_connectedChannels = QueryConnectedChannels()); + } + set + { + m_connectedChannels = value; + } + } + + [JsonIgnore] + [NonRecordField] + public List ConnectedAssets => Connections? + .SelectMany(connection => new[] { connection.Parent, connection.Child }) + .Where(asset => asset.ID != ID) + .ToList(); + + [JsonIgnore] + [NonRecordField] + public Func ConnectionFactory + { + get + { + return LazyContext.ConnectionFactory; + } + set + { + LazyContext.ConnectionFactory = value; + } + } + + #endregion + + #region [ Methods ] + + public IEnumerable GetAssetLocations(AdoDataConnection connection) + { + if (connection is null) + return null; + + TableOperations assetLocationTable = new TableOperations(connection); + return assetLocationTable.QueryRecordsWhere("AssetID = {0}", ID); + } + + private List QueryAssetLocations() + { + List assetLocations; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + assetLocations = GetAssetLocations(connection)? + .Select(LazyContext.GetAssetLocation) + .ToList(); + } + + if (!(assetLocations is null)) + { + foreach (AssetLocation assetLocation in assetLocations) + { + assetLocation.Asset = this; + assetLocation.LazyContext = LazyContext; + } + } + else + return new List(); + + return assetLocations; + } + + private List QueryMeterAssets() + { + List meterAssets; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + meterAssets = GetMeterAssets(connection)? + .Select(LazyContext.GetMeterAsset) + .ToList(); + } + + if (!(meterAssets is null)) + { + foreach (MeterAsset meterAsset in meterAssets) + { + meterAsset.Asset = this; + meterAsset.LazyContext = LazyContext; + } + } + + return meterAssets; + } + + private List QueryChannels() + { + List channels; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + channels = GetChannels(connection)? + .Select(LazyContext.GetChannel) + .ToList(); + } + + if (!(channels is null)) + { + foreach (Channel channel in channels) + { + channel.Asset = this; + channel.LazyContext = LazyContext; + } + } + + return channels; + } + + private List QueryConnectedChannels() + { + List channels; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + channels = GetConnectedChannels(connection)? + .Select(LazyContext.GetChannel) + .ToList(); + } + + if (!(channels is null)) + { + foreach (Channel channel in channels) + { + channel.LazyContext = LazyContext; + } + } + + return channels; + } + + private List QueryConnections() + { + List connections; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + connections = GetConnections(connection)? + .Select(LazyContext.GetAssetConnection) + .ToList(); + } + + if (!(connections is null)) + { + foreach (AssetConnection connection in connections) + { + connection.LazyContext = LazyContext; + } + } + + return connections; + } + + public IEnumerable GetMeterAssets(AdoDataConnection connection) + { + if (connection is null) + return null; + + TableOperations meterAssetTable = new TableOperations(connection); + return meterAssetTable.QueryRecordsWhere("AssetID = {0}", ID); + } + + public IEnumerable GetChannels(AdoDataConnection connection) + { + if (connection is null) + return null; + + TableOperations channelTable = new TableOperations(connection); + return channelTable.QueryRecordsWhere("AssetID = {0}", ID); + } + + public IEnumerable GetConnections(AdoDataConnection connection) + { + if (connection is null) + return null; + + TableOperations channelTable = new TableOperations(connection); + return channelTable.QueryRecordsWhere("ParentID = {0} OR ChildID = {1}", ID, ID); + } + + // Logic for Channels across Asset Connections + public IEnumerable GetConnectedChannels(AdoDataConnection connection) + { + if (connection is null) + return null; + + return AssetLocations + .SelectMany(assetLocation => TraverseConnectedChannels(connection, assetLocation.LocationID, ID)) + .Distinct(new ChannelComparer()); + } + + private IEnumerable TraverseConnectedChannels(AdoDataConnection connection, int locationID, int assetID) + { + TraversalContext context = new TraversalContext(connection, locationID); + ConnectedChannelLookup channelLookup = context.ConnectedChannelLookup; + context.VisitedAssets.Add(assetID); + + using (DataTable connectionTable = RetrieveConnectedAssets(connection, locationID, assetID)) + { + foreach (DataRow traversalRow in connectionTable.AsEnumerable()) + { + int connectedAssetID = traversalRow.ConvertField("ConnectedAssetID"); + string jumpSQL = traversalRow.ConvertField("JumpSQL"); + + IEnumerable jumpChannels = channelLookup(connectedAssetID, assetID, jumpSQL) + .Where(channel => channel.AssetID == connectedAssetID); + + context.ConnectedChannels.UnionWith(jumpChannels); + } + + foreach (DataRow traversalRow in connectionTable.AsEnumerable()) + { + int connectedAssetID = traversalRow.ConvertField("ConnectedAssetID"); + string passthroughSQL = traversalRow.ConvertField("PassthroughSQL"); + + List pathChannels = channelLookup(connectedAssetID, assetID, passthroughSQL) + .Where(channel => channel.AssetID != assetID) + .Except(context.ConnectedChannels) + .ToList(); + + if (pathChannels.Count == 0) + continue; + + TraverseConnectedChannels(context, pathChannels, connectedAssetID); + } + } + + return context.ConnectedChannels; + } + + private void TraverseConnectedChannels(TraversalContext context, List pathChannels, int visitedAssetID) + { + AdoDataConnection connection = context.Connection; + ConnectedChannelLookup channelLookup = context.ConnectedChannelLookup; + int locationID = context.LocationID; + + context.VisitedAssets.Add(visitedAssetID); + + using (DataTable connectionTable = RetrieveConnectedAssets(connection, locationID, visitedAssetID)) + { + foreach (DataRow traversalRow in connectionTable.AsEnumerable()) + { + int connectedAssetID = traversalRow.ConvertField("ConnectedAssetID"); + + if (context.VisitedAssets.Contains(connectedAssetID)) + continue; + + string jumpSQL = traversalRow.ConvertField("JumpSQL"); + + IEnumerable jumpChannels = channelLookup(connectedAssetID, visitedAssetID, jumpSQL) + .Where(channel => channel.AssetID == connectedAssetID) + .Intersect(pathChannels); + + context.ConnectedChannels.UnionWith(jumpChannels); + } + + HashSet filteredPathChannels = new HashSet(pathChannels); + filteredPathChannels.ExceptWith(context.ConnectedChannels); + + foreach (DataRow traversalRow in connectionTable.AsEnumerable()) + { + if (filteredPathChannels.Count == 0) + break; + + int connectedAssetID = traversalRow.ConvertField("ConnectedAssetID"); + + if (context.VisitedAssets.Contains(connectedAssetID)) + continue; + + string passthroughSQL = traversalRow.ConvertField("PassthroughSQL"); + + List passthroughChannels = channelLookup(connectedAssetID, visitedAssetID, passthroughSQL) + .Intersect(filteredPathChannels) + .ToList(); + + if (passthroughChannels.Count == 0) + continue; + + int connectedCount = context.ConnectedChannels.Count; + TraverseConnectedChannels(context, passthroughChannels, connectedAssetID); + + if (context.ConnectedChannels.Count != connectedCount) + filteredPathChannels.ExceptWith(context.ConnectedChannels); + } + } + + context.VisitedAssets.Remove(visitedAssetID); + } + + private DataTable RetrieveConnectedAssets(AdoDataConnection connection, int locationID, int visitedAssetID) + { + const string TraversalQueryFormat = + "SELECT DISTINCT " + + " ConnectedAsset.ID ConnectedAssetID, " + + " AssetRelationshipType.JumpConnection JumpSQL, " + + " AssetRelationshipType.PassThrough PassthroughSQL " + + "FROM " + + " Location JOIN " + + " Asset VisitedAsset ON " + + " Location.ID = {0} AND " + + " VisitedAsset.ID = {1} JOIN " + + " AssetConnection ON VisitedAsset.ID IN (AssetConnection.ParentID, AssetConnection.ChildID) JOIN " + + " Asset ConnectedAsset ON " + + " ConnectedAsset.ID IN (AssetConnection.ParentID, AssetConnection.ChildID) AND " + + " ConnectedAsset.ID <> VisitedAsset.ID JOIN " + + " AssetRelationshipType ON AssetConnection.AssetRelationshipTypeID = AssetRelationshipType.ID JOIN " + + " AssetLocation ON " + + " AssetLocation.AssetID = ConnectedAsset.ID AND " + + " AssetLocation.LocationID = Location.ID"; + + return connection.RetrieveData(TraversalQueryFormat, locationID, visitedAssetID); + } + + // Logic to find distance between two Assets + public int DistanceToAsset(int assetID) + { + int distance = 0; + HashSet visited = new HashSet(); + List next = new List() { this }; + + while (next.Count > 0) + { + if (next.Any(n => n.ID == assetID)) + return distance; + + foreach (Asset n in next) + visited.Add(n.ID); + + next = next + .SelectMany(n => n.ConnectedAssets) + .DistinctBy(n => n.ID) + .Where(n => !visited.Contains(n.ID)) + .ToList(); + + distance++; + } + + return -1; + } + + public T QueryAs(AdoDataConnection connection = null) where T : Asset, new() + { + if (this is T typedAsset) + return typedAsset; + + if (connection is null && ConnectionFactory is null) + throw new ArgumentNullException(nameof(connection)); + + Lazy lazyConnection = new Lazy(ConnectionFactory); + + try + { + TableOperations table = new TableOperations(connection ?? lazyConnection.Value); + return table.QueryRecordWhere("ID = {0}", ID); + } + finally + { + if (lazyConnection.IsValueCreated) + lazyConnection.Value.Dispose(); + } + } + + [Obsolete("Replaced by GetChannels")] + public IEnumerable GetChannel(AdoDataConnection connection) => GetChannels(connection); + + [Obsolete("Replaced by GetConnections")] + public IEnumerable GetConnection(AdoDataConnection connection) => GetConnections(connection); + + [Obsolete("Replaced by GetConnectedChannels")] + public IEnumerable GetConnectedChannel(AdoDataConnection connection) => GetConnectedChannels(connection); + + #endregion + + #region [ Static ] + + // Static Methods + private static ConnectedChannelLookup GetConnectedChannelLookup(AdoDataConnection connection, int locationID) + { + const string ChannelQueryFormat = + "SELECT SourceChannel.* " + + "FROM " + + " Channel SourceChannel JOIN " + + " Meter ON " + + " SourceChannel.MeterID = Meter.ID AND " + + " Meter.LocationID = {{0}} JOIN " + + " Asset ParentAsset ON ParentAsset.ID = {{1}} JOIN " + + " Asset ChildAsset ON ChildAsset.ID = {{2}} CROSS APPLY " + + " ({TraversalSQL}) Traversal(Traverse) " + + "WHERE Traversal.Traverse <> 0"; + + Dictionary channelLookup = new Dictionary(); + + var connectedChannelLookup = Enumerable + .Empty>() + .ToDictionary(_ => new { ParentID = 0, ChildID = 0, TraversalSQL = "" }); + + return (parentID, childID, traversalSQL) => + { + var key = new { ParentID = parentID, ChildID = childID, TraversalSQL = traversalSQL }; + return connectedChannelLookup.GetOrAdd(key, _ => RetrieveConnectedChannels(parentID, childID, traversalSQL)); + }; + + List RetrieveConnectedChannels(int parentID, int childID, string traversalSQL) + { + TableOperations channelTable = new TableOperations(connection); + + // lang=regex + const string Pattern = @"\{(?:parentid|childid|channelid)\}"; + string replacedSQL = Regex.Replace(traversalSQL, Pattern, ReplaceFormatParameter, RegexOptions.IgnoreCase); + + string channelQuery = ChannelQueryFormat + .Interpolate(new { TraversalSQL = replacedSQL }); + + return RetrieveRows(channelQuery, locationID, parentID, childID) + .Select(channelTable.LoadRecord) + .Select(LookUpChannel) + .ToList(); + } + + Channel LookUpChannel(Channel channel) => + channelLookup.GetOrAdd(channel.ID, _ => channel); + + string ReplaceFormatParameter(Match match) + { + switch (match.Value.ToLowerInvariant()) + { + case "{parentid}": return "ParentAsset.ID"; + case "{childid}": return "ChildAsset.ID"; + case "{channelid}": return "SourceChannel.ID"; + default: return match.Value; + } + } + + IEnumerable RetrieveRows(string query, params object[] args) + { + using (DataTable table = connection.RetrieveData(query, args)) + { + foreach (DataRow row in table.Rows) + yield return row; + } + } + } + + #endregion + } +} diff --git a/src/Libraries/openXDA.Model/TransmissionElements/BreakerOperation.cs b/src/Libraries/openXDA.Model/TransmissionElements/BreakerOperation.cs new file mode 100644 index 00000000..3be64ebe --- /dev/null +++ b/src/Libraries/openXDA.Model/TransmissionElements/BreakerOperation.cs @@ -0,0 +1,107 @@ +//****************************************************************************************************** +// BreakerOperation.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Data; +using Gemstone.Data; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + public class BreakerOperation + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int EventID { get; set; } + + public int PhaseID { get; set; } + + public int BreakerOperationTypeID { get; set; } + + public string BreakerNumber { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime TripCoilEnergized { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime StatusBitSet { get; set; } + + public bool StatusBitChatter { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime APhaseCleared { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime BPhaseCleared { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime CPhaseCleared { get; set; } + + public double BreakerTiming { get; set; } + + public double StatusTiming { get; set; } + + public double APhaseBreakerTiming { get; set; } + + public double BPhaseBreakerTiming { get; set; } + + public double CPhaseBreakerTiming { get; set; } + + public bool DcOffsetDetected { get; set; } + + public double BreakerSpeed { get; set; } + + public string UpdatedBy { get; set; } + } + + [TableName("BreakerOperation")] + public class BreakersForDay : BreakerOperation { } + + public class BreakerView + { + [PrimaryKey(true)] + public int ID { get; set; } + + public int MeterID { get; set; } + + public int EventID { get; set; } + + public string EventType { get; set; } + + public string Energized { get; set; } + + public int BreakerNumber { get; set; } + + public string LineName { get; set; } + + public string PhaseName { get; set; } + + public double Timing { get; set; } + + public int Speed { get; set; } + + public string OperationType { get; set; } + + public string UpdatedBy { get; set; } + } +} \ No newline at end of file diff --git a/src/Libraries/openXDA.Model/TransmissionElements/SourceImpedance.cs b/src/Libraries/openXDA.Model/TransmissionElements/SourceImpedance.cs new file mode 100644 index 00000000..998f1d16 --- /dev/null +++ b/src/Libraries/openXDA.Model/TransmissionElements/SourceImpedance.cs @@ -0,0 +1,114 @@ +//****************************************************************************************************** +// SourceImpedance.cs - Gbtc +// +// Copyright © 2017, 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 MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 06/19/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data; +using Gemstone.Data.Model; +using Newtonsoft.Json; + +namespace openXDA.Model +{ + [TableName("SourceImpedance")] + [PostRoles("Administrator, Transmission SME")] + [PatchRoles("Administrator, Transmission SME")] + [DeleteRoles("Administrator, Transmission SME")] + public class SourceImpedance + { + #region [ Members ] + + // Fields + private AssetLocation m_assetLocation; + + #endregion + + #region [ Properties ] + + [PrimaryKey(true)] + public int ID { get; set; } + + [ParentKey(typeof(AssetLocation))] + public int AssetLocationID { get; set; } + + public double RSrc { get; set; } + + public double XSrc { get; set; } + + [JsonIgnore] + [NonRecordField] + public AssetLocation AssetLocation + { + get + { + if (m_assetLocation is null) + m_assetLocation = LazyContext.GetAssetLocation(AssetLocationID); + + if (m_assetLocation is null) + m_assetLocation = QueryAssetLocation(); + + return m_assetLocation; + } + set => m_assetLocation = value; + } + + [JsonIgnore] + [NonRecordField] + public Func ConnectionFactory + { + get => LazyContext.ConnectionFactory; + set => LazyContext.ConnectionFactory = value; + } + + [JsonIgnore] + [NonRecordField] + internal LazyContext LazyContext { get; set; } = new LazyContext(); + + #endregion + + #region [ Methods ] + + public AssetLocation GetAssetLocation(AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations assetLocationTable = new TableOperations(connection); + return assetLocationTable.QueryRecordWhere("ID = {0}", AssetLocationID); + } + + private AssetLocation QueryAssetLocation() + { + AssetLocation assetLocation; + + using (AdoDataConnection connection = ConnectionFactory?.Invoke()) + { + assetLocation = GetAssetLocation(connection); + } + + if ((object)assetLocation != null) + assetLocation.LazyContext = LazyContext; + + return LazyContext.GetAssetLocation(assetLocation); + } + + #endregion + } +} diff --git a/src/Libraries/openXDA.Model/openXDA.Model.csproj b/src/Libraries/openXDA.Model/openXDA.Model.csproj new file mode 100644 index 00000000..68b24c0c --- /dev/null +++ b/src/Libraries/openXDA.Model/openXDA.Model.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + enable + enable + + + + + + + + + diff --git a/src/OpenSEE.sln b/src/OpenSEE.sln index 1423d43c..41c023a7 100644 --- a/src/OpenSEE.sln +++ b/src/OpenSEE.sln @@ -10,6 +10,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution OpenSEE\.eslintrc = OpenSEE\.eslintrc EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FaultAlgorithms", "Libraries\FaultAlgorithms\FaultAlgorithms.csproj", "{79E70E44-FB78-B280-97D7-1650186161DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FaultData", "Libraries\FaultData\FaultData.csproj", "{1C77C616-BE00-A5A0-8109-C0D9F7C0202E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "openXDA.Model", "Libraries\openXDA.Model\openXDA.Model.csproj", "{F463A000-041D-CC7D-0954-3942A5D4A946}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PQDS", "Libraries\PQDS\PQDS.csproj", "{C6E64BA2-DCA7-4A34-973A-2306A1F9EFFC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,10 +30,32 @@ Global {845F68F7-4094-4FE6-95E3-1B113BBFAD3F}.Debug|Any CPU.Build.0 = Debug|Any CPU {845F68F7-4094-4FE6-95E3-1B113BBFAD3F}.Release|Any CPU.ActiveCfg = Release|Any CPU {845F68F7-4094-4FE6-95E3-1B113BBFAD3F}.Release|Any CPU.Build.0 = Release|Any CPU + {79E70E44-FB78-B280-97D7-1650186161DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79E70E44-FB78-B280-97D7-1650186161DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79E70E44-FB78-B280-97D7-1650186161DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79E70E44-FB78-B280-97D7-1650186161DF}.Release|Any CPU.Build.0 = Release|Any CPU + {1C77C616-BE00-A5A0-8109-C0D9F7C0202E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C77C616-BE00-A5A0-8109-C0D9F7C0202E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C77C616-BE00-A5A0-8109-C0D9F7C0202E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C77C616-BE00-A5A0-8109-C0D9F7C0202E}.Release|Any CPU.Build.0 = Release|Any CPU + {F463A000-041D-CC7D-0954-3942A5D4A946}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F463A000-041D-CC7D-0954-3942A5D4A946}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F463A000-041D-CC7D-0954-3942A5D4A946}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F463A000-041D-CC7D-0954-3942A5D4A946}.Release|Any CPU.Build.0 = Release|Any CPU + {C6E64BA2-DCA7-4A34-973A-2306A1F9EFFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6E64BA2-DCA7-4A34-973A-2306A1F9EFFC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6E64BA2-DCA7-4A34-973A-2306A1F9EFFC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6E64BA2-DCA7-4A34-973A-2306A1F9EFFC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {79E70E44-FB78-B280-97D7-1650186161DF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {1C77C616-BE00-A5A0-8109-C0D9F7C0202E} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {F463A000-041D-CC7D-0954-3942A5D4A946} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {C6E64BA2-DCA7-4A34-973A-2306A1F9EFFC} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E81769A-8E91-4DE2-AD77-438C680437A5} EndGlobalSection diff --git a/src/OpenSEE/.eslintrc b/src/OpenSEE/.eslintrc index 7ff6c000..b6f09120 100644 --- a/src/OpenSEE/.eslintrc +++ b/src/OpenSEE/.eslintrc @@ -7,7 +7,8 @@ "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended" ], "rules": { "@typescript-eslint/no-namespace": "off", diff --git a/src/OpenSEE/App_Start/RouteConfig.cs b/src/OpenSEE/App_Start/RouteConfig.cs deleted file mode 100644 index 8efe6492..00000000 --- a/src/OpenSEE/App_Start/RouteConfig.cs +++ /dev/null @@ -1,70 +0,0 @@ -//****************************************************************************************************** -// RouteConfig.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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 02/19/2020 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.Mvc; -using System.Web.Routing; - -namespace OpenSEE -{ - public class RouteConfig - { - public static void RegisterRoutes(RouteCollection routes) - { - routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); - - routes.MapRoute( - name: "LoginRoute", - url: "Login", - defaults: new { controller = "Login", action = "Index" } - ); - - routes.MapRoute( - name: "AuthTestRoute", - url: "AuthTest", - defaults: new { controller = "Login", action = "AuthTest" } - ); - - routes.MapRoute( - name: "LogoutRoute", - url: "Logout", - defaults: new { controller = "Login", action = "Logout" } - ); - - routes.MapRoute( - name: "UserInfoRoute", - url: "UserInfo", - defaults: new { controller = "Login", action = "UserInfo" } - ); - - routes.MapRoute( - name: "Default", - url: "{controller}/{action}/{id}", - defaults: new { controller = "Home", action = "Home", id = UrlParameter.Optional } - ); - } - } -} diff --git a/src/OpenSEE/CSVDownload.ashx b/src/OpenSEE/CSVDownload.ashx deleted file mode 100644 index fb385e0f..00000000 --- a/src/OpenSEE/CSVDownload.ashx +++ /dev/null @@ -1 +0,0 @@ -<%@ WebHandler Language="C#" CodeBehind="CSVDownload.ashx.cs" Class="OpenSEE.CSVDownload" %> diff --git a/src/OpenSEE/Common.cs b/src/OpenSEE/Common.cs deleted file mode 100644 index 8742a38c..00000000 --- a/src/OpenSEE/Common.cs +++ /dev/null @@ -1,79 +0,0 @@ -//****************************************************************************************************** -// Common.cs - Gbtc -// -// Copyright © 2023, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 02/26/2023 - J. Ritchie Carroll -// Generated original version of source code. -// -//****************************************************************************************************** - -using System.IO; -using GSF; -using GSF.Configuration; -using GSF.IO; -using GSF.Reflection; -using GSF.Web.Security; - -namespace OpenSEE; - -public static class Common -{ - private static string s_applicationName; - private static string s_anonymousResourceExpression; - - public static string ApplicationName => s_applicationName ??= GetApplicationName(); - - public static string AnonymousResourceExpression => s_anonymousResourceExpression ??= GetAnonymousResourceExpression(); - - public static bool LogEnabled => GetLogEnabled(); - - public static string LogPath => GetLogPath(); - - public static int MaxLogFiles => GetMaxLogFiles(); - - private static string GetApplicationName() => - // Try database configured application name (if loaded yet) - MvcApplication.DefaultModel.Global.ApplicationName ?? - // Fall back on setting defined in web.config - GetSettingValue("SecurityProvider", "ApplicationName", "GSF Authentication"); - - private static string GetAnonymousResourceExpression() => - GetSettingValue("SystemSettings", "AnonymousResourceExpression", AuthenticationOptions.DefaultAnonymousResourceExpression); - - private static bool GetLogEnabled() => - GetSettingValue("SystemSettings", "LogEnabled", AssemblyInfo.ExecutingAssembly.Debuggable.ToString()).ParseBoolean(); - - private static string GetLogPath() => - GetSettingValue("SystemSettings", "LogPath", string.Format("{0}{1}Logs{1}", FilePath.GetAbsolutePath(""), Path.DirectorySeparatorChar)); - - private static int GetMaxLogFiles() => - int.TryParse(GetSettingValue("SystemSettings", "MaxLogFiles", "300"), out int maxLogFiles) ? maxLogFiles : 300; - - private static string GetSettingValue(string section, string keyName, string defaultValue) - { - try - { - ConfigurationFile config = ConfigurationFile.Current; - CategorizedSettingsElementCollection settings = config.Settings[section]; - return settings[keyName, true].ValueAs(defaultValue); - } - catch - { - return defaultValue; - } - } -} \ No newline at end of file diff --git a/src/OpenSEE/Content/FaultSpecifics.css b/src/OpenSEE/Content/FaultSpecifics.css deleted file mode 100644 index cc9d3137..00000000 --- a/src/OpenSEE/Content/FaultSpecifics.css +++ /dev/null @@ -1,37 +0,0 @@ -/****************************************************************************************************** -// FaultSpecifics.css - Gbtc -// -// Copyright © 2014, 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: -// ---------------------------------------------------------------------------------------------------- -// 04/14/2015 - Jeff Walker -// Generated original version of source code. -// -//******************************************************************************************************/ - - body { - margin: 0; - padding: 0; - height: 100%; - width: 100%; - overflow: hidden; - -webkit-user-select: none; - -moz-user-select: -moz-none; - -ms-user-select: none; - user-select: none; - font-size: 12px; - font-family: Lucida Grande, Lucida Sans, Arial, sans-serif; - } - diff --git a/src/OpenSEE/Controllers/AnalyticController.cs b/src/OpenSEE/Controllers/AnalyticController.cs index b1a9cadf..1d9b03d0 100644 --- a/src/OpenSEE/Controllers/AnalyticController.cs +++ b/src/OpenSEE/Controllers/AnalyticController.cs @@ -29,21 +29,22 @@ using System.Numerics; using System.Runtime.Caching; using System.Threading.Tasks; -using System.Web.Http; -using FaultData.DataAnalysis; -using GSF; -using GSF.Console; -using GSF.Data; -using GSF.Data.Model; -using GSF.NumericalAnalysis; -using GSF.Web; +using Gemstone.Data; +using Gemstone.Web; using MathNet.Numerics.IntegralTransforms; using OpenSEE.Model; +using Microsoft.AspNetCore.Mvc; +using Gemstone.Data.Model; +using Gemstone.Configuration; using openXDA.Model; +using FaultData.DataAnalysis; +using Microsoft.Extensions.Primitives; +using Gemstone.Data.DataExtensions; +using Gemstone.Numeric.Analysis; namespace OpenSEE { - [RoutePrefix("api/Analytic")] + [Route("api/Analytic")] public class AnalyticController : OpenSEEBaseController { #region [ Members ] @@ -59,13 +60,21 @@ public AnalyticController() : base() { } #endregion + private static string GetSourceTraceLabel(Channel channel, string trace = null) + { + string vi = channel.MeasurementType.Name == "Voltage" ? "V" : "I"; + string signal = trace; + string label = $"{channel.Asset.AssetName} {vi} {DisplayPhaseName(channel.Phase)}"; + return string.IsNullOrEmpty(signal) ? label : $"{label} {signal}"; + } + #region [ Static ] static AnalyticController() { s_memoryCache = new MemoryCache("Analytics"); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { m_cacheSlidingExpiration = connection.ExecuteScalar("SELECT Value FROM [OpenSee.Setting] WHERE Name = 'SlidingCacheExpiration'") ?? 2.0; } @@ -415,15 +424,13 @@ private class SequenceComponents [Route("GetFaultDistanceData"),HttpGet] public JsonReturn GetFaultDistanceData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - - int eventId = int.Parse(query["eventId"]); + int eventId = int.Parse(Request.Query["eventId"].ToString()); Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); Asset asset = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.AssetID); - meter.ConnectionFactory = () => new AdoDataConnection(connection.Connection, typeof(SqlDataAdapter), false); + meter.ConnectionFactory = () => new AdoDataConnection(Settings.Default); List returnList = new List(); @@ -444,7 +451,7 @@ public JsonReturn GetFaultDistanceData() private D3Series QueryFaultDistanceData(int faultCurveID, Meter meter, Asset asset, int evtID) { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { FaultCurve faultCurve = new TableOperations(connection).QueryRecordWhere("ID = {0}", faultCurveID); DataGroup dataGroup = new DataGroup(); @@ -484,21 +491,18 @@ private D3Series QueryFaultDistanceData(int faultCurveID, Meter meter, Asset ass [Route("GetFirstDerivativeData"), HttpGet] public async Task GetFirstDerivativeData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); + int eventId = int.Parse(Request.Query["eventId"].ToString()); List returnList = new List(); - DataTable table = connection.RetrieveData("SELECT ID, StartTime FROM Event WHERE ID = {0}", evt.ID); + DataTable table = connection.RetrieveData("SELECT ID, StartTime FROM Event WHERE ID = {0}", eventId); foreach (DataRow row in table.Rows) { int eventID = row.ConvertField("ID"); - DataGroup dataGroup = await QueryDataGroupAsync(eventId, meter); - VICycleDataGroup viCycleDataGroup = await QueryVICycleDataGroupAsync(eventID, meter); + // ToDo: this logic seems wrong, we look this up every time but it should be the same result, same eventId, unlike the line after... + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); + VICycleDataGroup viCycleDataGroup = await QueryVICycleDataGroupAsync(eventID, connection); returnList = returnList.Concat(GetFirstDerivativeLookup(dataGroup, viCycleDataGroup)).ToList(); } @@ -560,7 +564,7 @@ public static D3Series GetFirstDerivativeSeries(DataSeries dataSeries, string la { Unit = (dataSeries.SeriesInfo.Channel.MeasurementType.Name) + "perSecond", Color = GetColor(dataSeries.SeriesInfo.Channel), - LegendGroup = dataSeries.SeriesInfo.Channel.Asset.AssetKey, + LegendGroup = GetSourceTraceLabel(dataSeries.SeriesInfo.Channel, type), ChartLabel = dataSeries.SeriesInfo.Channel.Phase.Name + type + " First Derivative", LegendVertical = DisplayPhaseName(dataSeries.SeriesInfo.Channel.Phase), LegendHorizontal = type, @@ -600,16 +604,11 @@ public static D3Series GetFirstDerivativeSeries(DataSeries dataSeries, string la [Route("GetImpedanceData"), HttpGet] public async Task GetImpedanceData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); + int eventId = int.Parse(Request.Query["eventId"].ToString()); - VICycleDataGroup viCycleDataGroup = await QueryVICycleDataGroupAsync(evt.ID, meter); + VICycleDataGroup viCycleDataGroup = await QueryVICycleDataGroupAsync(eventId, connection); List returnList = GetImpedanceLookup(viCycleDataGroup); JsonReturn returnDict = new JsonReturn(); @@ -790,15 +789,10 @@ private IEnumerable CalculateImpedance(CycleDataGroup Voltage, CycleDat [Route("GetRemoveCurrentData"), HttpGet] public async Task GetRemoveCurrentData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); List returnList = GetRemoveCurrentLookup(dataGroup); JsonReturn returnDict = new JsonReturn(); @@ -837,7 +831,7 @@ public List GetRemoveCurrentLookup(DataGroup dataGroup) ChartLabel = GetChartLabel(item.SeriesInfo.Channel) + " Pre-Fault", Unit = "Current", Color = GetColor(item.SeriesInfo.Channel), - LegendGroup = item.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(item.SeriesInfo.Channel), DataMarker = new List(), BaseValue = GetIbase(Sbase, item.SeriesInfo.Channel.Asset.VoltageKV), DataPoints = fullWaveFormPre.Select((point, index) => new double[] { point.Time.Subtract(m_epoch).TotalMilliseconds, point.Value }).ToList() @@ -850,7 +844,7 @@ public List GetRemoveCurrentLookup(DataGroup dataGroup) ChartLabel = GetChartLabel(item.SeriesInfo.Channel) + " Post-Fault", Unit = "Current", Color = GetColor(item.SeriesInfo.Channel), - LegendGroup = item.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(item.SeriesInfo.Channel), DataMarker = new List(), BaseValue = GetIbase(Sbase, item.SeriesInfo.Channel.Asset.VoltageKV), DataPoints = fullWaveFormPost.Select((point, index) => new double[] { point.Time.Subtract(m_epoch).TotalMilliseconds, point.Value }).ToList() @@ -877,7 +871,7 @@ public List GetRemoveCurrentLookup(DataGroup dataGroup) ChartLabel = GetChartLabel(item.SeriesInfo.Channel) + " Pre-Fault", Unit = "Current", Color = GetColor(item.SeriesInfo.Channel), - LegendGroup = item.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(item.SeriesInfo.Channel), DataMarker = new List(), BaseValue = GetIbase(Sbase, item.SeriesInfo.Channel.Asset.VoltageKV), DataPoints = fullWaveFormPre.Select((point, index) => new double[] { point.Time.Subtract(m_epoch).TotalMilliseconds, point.Value }).ToList() @@ -890,7 +884,7 @@ public List GetRemoveCurrentLookup(DataGroup dataGroup) ChartLabel = GetChartLabel(item.SeriesInfo.Channel) + " Post-Fault", Unit = "Current", Color = GetColor(item.SeriesInfo.Channel), - LegendGroup = item.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(item.SeriesInfo.Channel), DataMarker = new List(), BaseValue = GetIbase(Sbase, item.SeriesInfo.Channel.Asset.VoltageKV), DataPoints = fullWaveFormPost.Select((point, index) => new double[] { point.Time.Subtract(m_epoch).TotalMilliseconds, point.Value }).ToList() @@ -915,7 +909,7 @@ public List GetRemoveCurrentLookup(DataGroup dataGroup) ChartLabel = GetChartLabel(item.SeriesInfo.Channel) + " Pre-Fault", Unit = "Current", Color = GetColor(item.SeriesInfo.Channel), - LegendGroup = item.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(item.SeriesInfo.Channel), DataMarker = new List(), BaseValue = GetIbase(Sbase, item.SeriesInfo.Channel.Asset.VoltageKV), DataPoints = fullWaveFormPre.Select((point, index) => new double[] { point.Time.Subtract(m_epoch).TotalMilliseconds, point.Value }).ToList() @@ -928,7 +922,7 @@ public List GetRemoveCurrentLookup(DataGroup dataGroup) ChartLabel = GetChartLabel(item.SeriesInfo.Channel) + " Post-Fault", Unit = "Current", Color = GetColor(item.SeriesInfo.Channel), - LegendGroup = item.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(item.SeriesInfo.Channel), DataMarker = new List(), BaseValue = GetIbase(Sbase, item.SeriesInfo.Channel.Asset.VoltageKV), DataPoints = fullWaveFormPost.Select((point, index) => new double[] { point.Time.Subtract(m_epoch).TotalMilliseconds, point.Value }).ToList() @@ -945,15 +939,10 @@ public List GetRemoveCurrentLookup(DataGroup dataGroup) [Route("GetI2tData"), HttpGet] public async Task GetI2tData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); List returnList = GetI2tLookup(dataGroup); JsonReturn returnDict = new JsonReturn(); @@ -981,10 +970,10 @@ public List GetI2tLookup(DataGroup dataGroup) LegendVGroup = "", LegendHorizontal = "", LegendVertical = DisplayPhaseName(item.SeriesInfo.Channel.Phase), - ChartLabel = GetChartLabel(item.SeriesInfo.Channel) + " I2t", + ChartLabel = GetChartLabel(item.SeriesInfo.Channel) + " I2T", Unit = "Current", Color = GetColor(item.SeriesInfo.Channel), - LegendGroup = item.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(item.SeriesInfo.Channel), DataMarker = new List(), BaseValue = GetIbase(Sbase, item.SeriesInfo.Channel.Asset.VoltageKV), DataPoints = ComputeI2T(item.DataPoints,1.0/(double)item.SampleRate).ToList() @@ -1012,16 +1001,10 @@ private IEnumerable ComputeI2T(IEnumerable current, double [Route("GetPowerData"), HttpGet] public async Task GetPowerData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - VICycleDataGroup vICycleDataGroup = await QueryVICycleDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + VICycleDataGroup vICycleDataGroup = await QueryVICycleDataGroupAsync(eventId, connection); List returnList = GetPowerLookup(vICycleDataGroup); JsonReturn returnDict = new JsonReturn(); @@ -1093,10 +1076,10 @@ public List GetPowerLookup(VICycleDataGroup vICycleDataGroup) }); dataLookup.Add(new D3Series() { - LegendHorizontal = "Pf", + LegendHorizontal = "PF", LegendVertical = "AN", Unit = "PowerPf", - Color = "Pfa", + Color = "PFa", LegendVGroup = "", LegendGroup = vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.AssetName, BaseValue = 1.0, @@ -1160,10 +1143,10 @@ public List GetPowerLookup(VICycleDataGroup vICycleDataGroup) }); dataLookup.Add(new D3Series() { - LegendHorizontal = "Pf", + LegendHorizontal = "PF", LegendVertical = "BN", Unit = "PowerPf", - Color = "Pfb", + Color = "PFb", LegendVGroup = "", LegendGroup = vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.AssetName, BaseValue = 1.0, @@ -1226,10 +1209,10 @@ public List GetPowerLookup(VICycleDataGroup vICycleDataGroup) }); dataLookup.Add(new D3Series() { - LegendHorizontal = "Pf", + LegendHorizontal = "PF", LegendVertical = "CN", Unit = "PowerPf", - Color = "Pfc", + Color = "PFc", LegendVGroup = "", LegendGroup = vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.AssetName, BaseValue = 1.0, @@ -1287,10 +1270,10 @@ public List GetPowerLookup(VICycleDataGroup vICycleDataGroup) }); dataLookup.Add(new D3Series() { - LegendHorizontal = "Pf", + LegendHorizontal = "PF", LegendVertical = "Total", Unit = "PowerPf", - Color = "Pft", + Color = "PFt", LegendVGroup = "", LegendGroup = vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.AssetName, BaseValue = 1.0, @@ -1309,15 +1292,10 @@ public List GetPowerLookup(VICycleDataGroup vICycleDataGroup) [Route("GetMissingVoltageData"), HttpGet] public async Task GetMissingVoltageData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); List returnList = GetMissingVoltageLookup(dataGroup); JsonReturn returnDict = new JsonReturn(); @@ -1357,7 +1335,7 @@ public List GetMissingVoltageLookup(DataGroup dataGroup) LegendVGroup = "", LegendVertical = DisplayPhaseName(ds.SeriesInfo.Channel.Phase), LegendHorizontal = "Pre", - LegendGroup = ds.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(ds.SeriesInfo.Channel), BaseValue = ds.SeriesInfo.Channel.Asset.VoltageKV, DataMarker = new List(), DataPoints = fullWaveFormPre.Select((point, index) => new double[] { point.Time.Subtract(m_epoch).TotalMilliseconds, point.Value }).ToList() @@ -1371,7 +1349,7 @@ public List GetMissingVoltageLookup(DataGroup dataGroup) LegendVGroup = "", LegendVertical = DisplayPhaseName(ds.SeriesInfo.Channel.Phase), LegendHorizontal = "Post", - LegendGroup = ds.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(ds.SeriesInfo.Channel), BaseValue = ds.SeriesInfo.Channel.Asset.VoltageKV, DataMarker = new List(), DataPoints = fullWaveFormPost.Select((point, index) => new double[] { point.Time.Subtract(m_epoch).TotalMilliseconds, point.Value }).ToList() @@ -1390,15 +1368,10 @@ public List GetMissingVoltageLookup(DataGroup dataGroup) [Route("GetClippedWaveformsData"), HttpGet] public async Task GetClippedWaveformsData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); List returnList = GetClippedWaveformsLookup(dataGroup); JsonReturn returnDict = new JsonReturn(); @@ -1445,7 +1418,7 @@ private static D3Series GenerateFixedWaveform(DataSeries dataSeries, string labe Unit = dataSeries.SeriesInfo.Channel.MeasurementType.Name, Color = GetColor(dataSeries.SeriesInfo.Channel), BaseValue = (type == "V" ? dataSeries.SeriesInfo.Channel.Asset.VoltageKV : GetIbase(Sbase, dataSeries.SeriesInfo.Channel.Asset.VoltageKV)), - LegendGroup = dataSeries.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(dataSeries.SeriesInfo.Channel), DataMarker = new List(), LegendVertical = DisplayPhaseName(dataSeries.SeriesInfo.Channel.Phase), LegendHorizontal = type, @@ -1532,17 +1505,11 @@ private static D3Series GenerateFixedWaveform(DataSeries dataSeries, string labe [Route("GetLowPassFilterData"), HttpGet] public async Task GetLowPassFilterData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int filterOrder = int.Parse(query["filter"]); - int eventId = int.Parse(query["eventId"]); - - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, meter); + int filterOrder = int.Parse(Request.Query["filter"].ToString()); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); List returnList = GetLowPassFilterLookup(dataGroup, filterOrder); JsonReturn returnDict = new JsonReturn(); @@ -1590,7 +1557,7 @@ public D3Series FilteredPassSignal(Filter Filt, DataSeries Data) Unit = Data.SeriesInfo.Channel.MeasurementType.Name, Color = GetColor(Data.SeriesInfo.Channel), BaseValue = (Data.SeriesInfo.Channel.MeasurementType.Name == "Voltage" ? Data.SeriesInfo.Channel.Asset.VoltageKV : GetIbase(Sbase, Data.SeriesInfo.Channel.Asset.VoltageKV)), - LegendGroup = Data.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(Data.SeriesInfo.Channel), DataMarker = new List(), LegendVertical = DisplayPhaseName(Data.SeriesInfo.Channel.Phase), LegendHorizontal = (Data.SeriesInfo.Channel.MeasurementType.Name == "Voltage" ? "V" : "I"), @@ -1607,17 +1574,11 @@ public D3Series FilteredPassSignal(Filter Filt, DataSeries Data) [Route("GetHighPassFilterData"), HttpGet] public async Task GetHighPassFilterData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int filterOrder = int.Parse(query["filter"]); - int eventId = int.Parse(query["eventId"]); - - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, meter); + int filterOrder = int.Parse(Request.Query["filter"].ToString()); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); List returnList = GetHighPassFilterLookup(dataGroup, filterOrder); JsonReturn returnDict = new JsonReturn(); @@ -1660,15 +1621,10 @@ public List GetHighPassFilterLookup(DataGroup dataGroup, int order) [Route("GetOverlappingWaveformData"), HttpGet] public async Task GetOverlappingWaveformData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); List returnList = GetOverlappingWaveformLookup(dataGroup); JsonReturn returnDict = new JsonReturn(); @@ -1714,7 +1670,7 @@ private D3Series GenerateOverlappingWaveform(DataSeries dataSeries) Unit = dataSeries.SeriesInfo.Channel.MeasurementType.Name, Color = GetColor(dataSeries.SeriesInfo.Channel), BaseValue = (dataSeries.SeriesInfo.Channel.MeasurementType.Name == "Voltage" ? dataSeries.SeriesInfo.Channel.Asset.VoltageKV : GetIbase(Sbase, dataSeries.SeriesInfo.Channel.Asset.VoltageKV)), - LegendGroup = dataSeries.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(dataSeries.SeriesInfo.Channel), DataMarker = new List(), LegendVertical = DisplayPhaseName(dataSeries.SeriesInfo.Channel.Phase), LegendHorizontal = (dataSeries.SeriesInfo.Channel.MeasurementType.Name == "Voltage" ? "V" : "I"), @@ -1756,16 +1712,10 @@ private D3Series GenerateOverlappingWaveform(DataSeries dataSeries) [Route("GetRapidVoltageChangeData"), HttpGet] public async Task GetRapidVoltageChangeData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - VICycleDataGroup vICycleDataGroup = await QueryVICycleDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + VICycleDataGroup vICycleDataGroup = await QueryVICycleDataGroupAsync(eventId, connection); List returnList = GetRapidVoltageChangeLookup(vICycleDataGroup); JsonReturn returnDict = new JsonReturn(); @@ -1803,7 +1753,7 @@ private D3Series GetRapidVoltageChangeFlotSeries(DataSeries dataSeries) Unit = "Voltage", Color = GetColor(dataSeries.SeriesInfo.Channel), BaseValue = dataSeries.SeriesInfo.Channel.Asset.VoltageKV, - LegendGroup = dataSeries.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(dataSeries.SeriesInfo.Channel, "RMS"), DataMarker = new List(), LegendVertical = DisplayPhaseName(dataSeries.SeriesInfo.Channel.Phase), LegendHorizontal = "", @@ -1838,16 +1788,10 @@ private D3Series GetRapidVoltageChangeFlotSeries(DataSeries dataSeries) [Route("GetSymmetricalComponentsData"), HttpGet] public async Task GetSymmetricalComponentsData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - VICycleDataGroup vICycleDataGroup = await QueryVICycleDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + VICycleDataGroup vICycleDataGroup = await QueryVICycleDataGroupAsync(eventId, connection); List returnList = GetSymmetricalComponentsLookup(vICycleDataGroup); JsonReturn returnDict = new JsonReturn(); @@ -1891,7 +1835,7 @@ public List GetSymmetricalComponentsLookup(VICycleDataGroup vICycleDat dataLookup.Add(new D3Series() { Unit = "Voltage", - Color = "VS0", + Color = "VZero", BaseValue = vICycleDataGroup.VA.RMS.SeriesInfo.Channel.Asset.VoltageKV, LegendGroup = vICycleDataGroup.VA.RMS.SeriesInfo.Channel.Asset.AssetName, DataMarker = new List(), @@ -1904,7 +1848,7 @@ public List GetSymmetricalComponentsLookup(VICycleDataGroup vICycleDat dataLookup.Add(new D3Series() { Unit = "Voltage", - Color = "VS1", + Color = "VPos", BaseValue = vICycleDataGroup.VA.RMS.SeriesInfo.Channel.Asset.VoltageKV, LegendGroup = vICycleDataGroup.VA.RMS.SeriesInfo.Channel.Asset.AssetName, DataMarker = new List(), @@ -1916,7 +1860,7 @@ public List GetSymmetricalComponentsLookup(VICycleDataGroup vICycleDat dataLookup.Add(new D3Series() { Unit = "Voltage", - Color = "VS2", + Color = "VNeg", BaseValue = vICycleDataGroup.VA.RMS.SeriesInfo.Channel.Asset.VoltageKV, LegendGroup = vICycleDataGroup.VA.RMS.SeriesInfo.Channel.Asset.AssetName, DataMarker = new List(), @@ -1960,7 +1904,7 @@ public List GetSymmetricalComponentsLookup(VICycleDataGroup vICycleDat dataLookup.Add(new D3Series() { Unit = "Current", - Color = "IS0", + Color = "IZero", BaseValue = GetIbase(Sbase,vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.VoltageKV), LegendGroup = vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.AssetName, DataMarker = new List(), @@ -1973,7 +1917,7 @@ public List GetSymmetricalComponentsLookup(VICycleDataGroup vICycleDat dataLookup.Add(new D3Series() { Unit = "Current", - Color = "IS1", + Color = "IPos", BaseValue = GetIbase(Sbase, vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.VoltageKV), LegendGroup = vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.AssetName, DataMarker = new List(), @@ -1985,7 +1929,7 @@ public List GetSymmetricalComponentsLookup(VICycleDataGroup vICycleDat dataLookup.Add(new D3Series() { Unit = "Current", - Color = "IS2", + Color = "INeg", BaseValue = GetIbase(Sbase, vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.VoltageKV), LegendGroup = vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.AssetName, DataMarker = new List(), @@ -2023,16 +1967,10 @@ private SequenceComponents CalculateSequenceComponents(Complex an, Complex bn, C [Route("GetUnbalanceData"), HttpGet] public async Task GetUnbalanceData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - VICycleDataGroup vICycleDataGroup = await QueryVICycleDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + VICycleDataGroup vICycleDataGroup = await QueryVICycleDataGroupAsync(eventId, connection); List returnList = GetUnbalanceLookup(vICycleDataGroup); JsonReturn returnDict = new JsonReturn(); @@ -2076,11 +2014,11 @@ public List GetUnbalanceLookup(VICycleDataGroup vICycleDataGroup) dataLookup.Add(new D3Series() { Unit = "Unbalance", - Color = "VS0", + Color = "VZero", BaseValue = vICycleDataGroup.VA.RMS.SeriesInfo.Channel.Asset.VoltageKV, LegendGroup = vICycleDataGroup.VA.RMS.SeriesInfo.Channel.Asset.AssetName, DataMarker = new List(), - LegendVertical = "S0/S1", + LegendVertical = "Zero/Pos", LegendHorizontal = "V", LegendVGroup = "", DataPoints = sequencComponents.Select((point, index) => new double[] { va[index].Time.Subtract(m_epoch).TotalMilliseconds, point.S0.Magnitude / point.S1.Magnitude }).ToList() @@ -2090,11 +2028,11 @@ public List GetUnbalanceLookup(VICycleDataGroup vICycleDataGroup) dataLookup.Add(new D3Series() { Unit = "Unbalance", - Color = "VS2", + Color = "VNeg", BaseValue = vICycleDataGroup.VA.RMS.SeriesInfo.Channel.Asset.VoltageKV, LegendGroup = vICycleDataGroup.VA.RMS.SeriesInfo.Channel.Asset.AssetName, DataMarker = new List(), - LegendVertical = "S2/S1", + LegendVertical = "Neg/Pos", LegendHorizontal = "V", LegendVGroup = "", DataPoints = sequencComponents.Select((point, index) => new double[] { va[index].Time.Subtract(m_epoch).TotalMilliseconds, point.S2.Magnitude / point.S1.Magnitude }).ToList() @@ -2136,11 +2074,11 @@ public List GetUnbalanceLookup(VICycleDataGroup vICycleDataGroup) dataLookup.Add(new D3Series() { Unit = "Unbalance", - Color = "IS0", + Color = "IZero", BaseValue = 1.0, LegendGroup = vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.AssetName, DataMarker = new List(), - LegendVertical = "S0/S1", + LegendVertical = "Zero/Pos", LegendHorizontal = "I", LegendVGroup = "", DataPoints = sequencComponents.Select((point, index) => new double[] { ia[index].Time.Subtract(m_epoch).TotalMilliseconds, point.S0.Magnitude / point.S1.Magnitude }).ToList() @@ -2150,11 +2088,11 @@ public List GetUnbalanceLookup(VICycleDataGroup vICycleDataGroup) dataLookup.Add(new D3Series() { Unit = "Unbalance", - Color = "IS2", + Color = "INeg", BaseValue = 1.0, LegendGroup = vICycleDataGroup.IA.RMS.SeriesInfo.Channel.Asset.AssetName, DataMarker = new List(), - LegendVertical = "S2/S1", + LegendVertical = "Neg/Pos", LegendHorizontal = "I", LegendVGroup = "", DataPoints = sequencComponents.Select((point, index) => new double[] { ia[index].Time.Subtract(m_epoch).TotalMilliseconds, point.S2.Magnitude / point.S1.Magnitude }).ToList() @@ -2174,17 +2112,11 @@ public List GetUnbalanceLookup(VICycleDataGroup vICycleDataGroup) [Route("GetRectifierData"), HttpGet] public async Task GetRectifierData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - double TRC = double.Parse(query["Trc"]); - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - double systemFrequency = connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'SystemFrequency'") ?? 60.0; - VIDataGroup dataGroup = await QueryVIDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + double TRC = double.Parse(Request.Query["Trc"].ToString()); + VIDataGroup dataGroup = await QueryVIDataGroupAsync(eventId, connection); List returnList = GetRectifierLookup(dataGroup, TRC); JsonReturn returnDict = new JsonReturn(); @@ -2271,15 +2203,10 @@ public List GetRectifierLookup(VIDataGroup dataGroup, double RC) [Route("GetFrequencyData"), HttpGet] public async Task GetFrequencyData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - VIDataGroup viDataGroup = await QueryVIDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + VIDataGroup viDataGroup = await QueryVIDataGroupAsync(eventId, connection); List returnList = GetFrequencyLookup(viDataGroup); JsonReturn returnDict = new JsonReturn(); @@ -2341,7 +2268,7 @@ private D3Series GenerateFrequency(DataSeries dataSeries, string label) Unit = "Freq", Color = GetFrequencyColor(dataSeries.SeriesInfo.Channel.Phase.Name), BaseValue = Fbase, - LegendGroup = dataSeries.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(dataSeries.SeriesInfo.Channel), DataMarker = new List(), LegendVertical = DisplayPhaseName(dataSeries.SeriesInfo.Channel.Phase), LegendHorizontal = "", @@ -2440,16 +2367,14 @@ private D3Series MedianFilt(D3Series input) [Route("GetTHDData"), HttpGet] public async Task GetTHDData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - int forceFullRes = int.Parse(query.ContainsKey("fullRes") ? query["fullRes"] : "0"); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + int forceFullRes = 0; + if (Request.Query.TryGetValue("fullRes", out StringValues fullRes)) + forceFullRes = int.Parse(fullRes.ToString()); - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, meter); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); List returnList = GetTHDLookup(dataGroup, forceFullRes==1); JsonReturn returnDict = new JsonReturn(); @@ -2493,7 +2418,7 @@ private D3Series GenerateTHD( DataSeries dataSeries, bool fullRes) Unit = "THD", Color = GetColor(dataSeries.SeriesInfo.Channel), BaseValue = 1, - LegendGroup = dataSeries.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(dataSeries.SeriesInfo.Channel), DataMarker = new List(), LegendVertical = DisplayPhaseName(dataSeries.SeriesInfo.Channel.Phase), LegendHorizontal = (dataSeries.SeriesInfo.Channel.MeasurementType.Name == "Voltage"? "V" : "I"), @@ -2542,18 +2467,15 @@ private D3Series GenerateTHD( DataSeries dataSeries, bool fullRes) [Route("GetSpecifiedHarmonicData"), HttpGet] public async Task GetSpecifiedHarmonicData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - int forceFullRes = int.Parse(query.ContainsKey("fullRes") ? query["fullRes"] : "0"); - - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - int specifiedHarmonic = int.Parse(query["specifiedHarmonic"]); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + int forceFullRes = 0; + if (Request.Query.TryGetValue("fullRes", out StringValues fullRes)) + forceFullRes = int.Parse(fullRes.ToString()); + int specifiedHarmonic = int.Parse(Request.Query["specifiedHarmonic"].ToString()); - DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, meter); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); List returnList = GetSpecifiedHarmonicLookup(dataGroup, specifiedHarmonic, forceFullRes==1); JsonReturn returnDict = new JsonReturn(); @@ -2595,7 +2517,7 @@ private static IEnumerable GenerateSpecifiedHarmonic( DataSeries dataS Unit = dataSeries.SeriesInfo.Channel.MeasurementType.Name, Color = GetColor(dataSeries.SeriesInfo.Channel), BaseValue = (dataSeries.SeriesInfo.Channel.MeasurementType.Name == "Voltage"? dataSeries.SeriesInfo.Channel.Asset.VoltageKV : GetIbase(Sbase, dataSeries.SeriesInfo.Channel.Asset.VoltageKV)), - LegendGroup = dataSeries.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(dataSeries.SeriesInfo.Channel), DataMarker = new List(), LegendVertical = DisplayPhaseName(dataSeries.SeriesInfo.Channel.Phase), LegendHorizontal = "Mag", @@ -2608,7 +2530,7 @@ private static IEnumerable GenerateSpecifiedHarmonic( DataSeries dataS Unit = "Angle", Color = GetColor(dataSeries.SeriesInfo.Channel), BaseValue = (dataSeries.SeriesInfo.Channel.MeasurementType.Name == "Voltage" ? dataSeries.SeriesInfo.Channel.Asset.VoltageKV : GetIbase(Sbase, dataSeries.SeriesInfo.Channel.Asset.VoltageKV)), - LegendGroup = dataSeries.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(dataSeries.SeriesInfo.Channel), DataMarker = new List(), LegendVertical = DisplayPhaseName(dataSeries.SeriesInfo.Channel.Phase), LegendHorizontal = "Ph", @@ -2667,17 +2589,11 @@ private static IEnumerable GenerateSpecifiedHarmonic( DataSeries dataS [Route("GetBreakerRestrikeData"), HttpGet] public async Task GetBreakerRestrikeData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - - DataGroup dataGroup = await QueryDataGroupAsync(eventId, meter); - List returnList = GetBreakerRestrikeData(evt.ID, dataGroup); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); + List returnList = GetBreakerRestrikeData(eventId, dataGroup); JsonReturn returnDict = new JsonReturn(); returnDict.Data = returnList; @@ -2687,7 +2603,7 @@ public async Task GetBreakerRestrikeData() private List GetBreakerRestrikeData(int eventID, DataGroup dataGroup) { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { List currents; List voltages; @@ -2768,18 +2684,23 @@ private List GetBreakerRestrikeData(int eventID, DataGroup dataGroup) [Route("GetFFTData"), HttpGet] public async Task GetFFTData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - int cycles = query.ContainsKey("cycles") ? int.Parse(query["cycles"]) : 1; + int eventId = int.Parse(Request.Query["eventId"].ToString()); + int cycles = 1; + if (Request.Query.TryGetValue("cycles", out StringValues cycleString)) + cycles = int.Parse(cycleString.ToString()); - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); + double startTime; + if (Request.Query.TryGetValue("startDate", out StringValues start)) + startTime = double.Parse(start.ToString()); + else + { + Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); + startTime = evt.StartTime.Subtract(m_epoch).TotalMilliseconds; + } - double startTime = query.ContainsKey("startDate") ? double.Parse(query["startDate"]) : evt.StartTime.Subtract(m_epoch).TotalMilliseconds; - DataGroup dataGroup = await QueryDataGroupAsync(eventId, meter); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); List returnList = GetFFTLookup(dataGroup, startTime, cycles); JsonReturn returnDict = new JsonReturn(); @@ -2792,7 +2713,7 @@ public List GetFFTLookup(DataGroup dataGroup, double startTime, int cy { int maxHarmonic; - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) maxHarmonic = int.Parse(connection.ExecuteScalar("SELECT Value FROM [OpenSee.Setting] WHERE Name = 'maxFFTHarmonic'") ?? "50"); List dataLookup = new List(); @@ -2825,7 +2746,7 @@ private List GenerateFFT(DataSeries dataSeries, double startTime, int Unit = dataSeries.SeriesInfo.Channel.MeasurementType.Name, Color = GetColor(dataSeries.SeriesInfo.Channel), BaseValue = (dataSeries.SeriesInfo.Channel.MeasurementType.Name == "Voltage" ? GetBaseV(dataSeries.SeriesInfo.Channel, false) * 1000.0 : GetIbase(Sbase, dataSeries.SeriesInfo.Channel.Asset.VoltageKV)), - LegendGroup = dataSeries.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(dataSeries.SeriesInfo.Channel), DataMarker = new List(), LegendVertical = DisplayPhaseName(dataSeries.SeriesInfo.Channel.Phase), LegendHorizontal = "Mag", @@ -2838,7 +2759,7 @@ private List GenerateFFT(DataSeries dataSeries, double startTime, int Unit = "Angle", Color = GetColor(dataSeries.SeriesInfo.Channel), BaseValue = 1.0, - LegendGroup = dataSeries.SeriesInfo.Channel.Asset.AssetName, + LegendGroup = GetSourceTraceLabel(dataSeries.SeriesInfo.Channel), DataMarker = new List(), LegendVertical = DisplayPhaseName(dataSeries.SeriesInfo.Channel.Phase), LegendHorizontal = "Ang", @@ -2880,4 +2801,4 @@ private List GenerateFFT(DataSeries dataSeries, double startTime, int #endregion } -} \ No newline at end of file +} diff --git a/src/OpenSEE/CSVDownload.ashx.cs b/src/OpenSEE/Controllers/CSVController.cs similarity index 52% rename from src/OpenSEE/CSVDownload.ashx.cs rename to src/OpenSEE/Controllers/CSVController.cs index 9f4f7d14..091617c6 100644 --- a/src/OpenSEE/CSVDownload.ashx.cs +++ b/src/OpenSEE/Controllers/CSVController.cs @@ -1,5 +1,5 @@ //****************************************************************************************************** -// OpenSEECSVDownload.ashx.cs - Gbtc +// CSVController.cs - Gbtc // // Copyright © 2018, Grid Protection Alliance. All Rights Reserved. // @@ -18,165 +18,111 @@ // ---------------------------------------------------------------------------------------------------- // 11/06/2018 - Billy Ernest // Generated original version of source code. +// 02/05/2026 - Gabriel Santos +// Refactored code to be in controller format for netcore upgrade. // //****************************************************************************************************** using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Data; using System.IO; using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using System.Web; using FaultData.DataAnalysis; -using GSF.Collections; -using GSF.Data; -using GSF.Data.Model; -using GSF.Threading; +using Gemstone.Configuration; +using Gemstone.Data; +using Gemstone.Data.Model; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using OpenSEE.Model; using openXDA.Model; -using CancellationToken = System.Threading.CancellationToken; namespace OpenSEE { /// - /// Summary description for OpenSEECSVDownload + /// An MVC controller for downloading /// - public class CSVDownload : IHttpHandler + [Route("api/CSV")] + public class CSVController : Controller { - #region [ Members ] - // Fields private DateTime m_epoch = new DateTime(1970, 1, 1); - // Nested Types - private class HttpResponseCancellationToken : CompatibleCancellationToken - { - private readonly HttpResponse m_reponse; - - public HttpResponseCancellationToken(HttpResponse response) : base(CancellationToken.None) - { - m_reponse = response; - } - - public override bool IsCancelled => !m_reponse.IsClientConnected; - } - - const string CsvContentType = "text/csv"; - - #endregion - #region [ Properties ] - - /// - /// Gets a value indicating whether another request can use the instance. - /// - /// - /// true if the instance is reusable; otherwise, false. - /// - public bool IsReusable => false; + #region [ Methods ] /// - /// Determines if client cache should be enabled for rendered handler content. + /// This is just with slightly different dispose logic. /// /// - /// If rendered handler content does not change often, the server and client will use the - /// to determine if the client needs to refresh the content. + /// MVC owns the so we shouldn't close it. /// - public bool UseClientCache => false; - - public string Filename { get; private set; } - #endregion - - #region [ Methods ] - - public void ProcessRequest(HttpContext context) + private class NoCloseStreamWriter : StreamWriter { - HttpResponse response = HttpContext.Current.Response; - HttpResponseCancellationToken cancellationToken = new HttpResponseCancellationToken(response); - NameValueCollection requestParameters = context.Request.QueryString; + public NoCloseStreamWriter(Stream stream) : base(stream) { } - try + protected override void Dispose(bool disposing) { - Filename = (requestParameters["Meter"] == null? "" : (requestParameters["Meter"] + "_")) + (requestParameters["EventType"] == null? "" : requestParameters["EventType"] + "_") + "Event_" + requestParameters["eventID"] + ".csv"; - if (requestParameters["type"] == "pqds") - Filename = "PQDS_" + Filename; - response.ClearContent(); - response.Clear(); - response.AddHeader("Content-Type", CsvContentType); - response.AddHeader("Content-Disposition", "attachment;filename=" + Filename); - response.BufferOutput = true; - - WriteTableToStream(requestParameters, response.OutputStream, response.Flush, cancellationToken); - } - catch (Exception e) - { - LogExceptionHandler?.Invoke(e); - throw; - } - finally - { - response.End(); + base.Dispose(false); } } - public Task ProcessRequestAsync(HttpRequestMessage request, HttpResponseMessage response, CancellationToken cancellationToken) + [HttpGet, Route("Download")] + public ActionResult GetCSV() { - NameValueCollection requestParameters = request.RequestUri.ParseQueryString(); + IQueryCollection requestParameters = Request.Query; - response.Content = new PushStreamContent((stream, content, context) => - { - try - { - Filename = (requestParameters["Meter"] == null ? "" : (requestParameters["Meter"] + "_")) + (requestParameters["EventType"] == null ? "" : requestParameters["EventType"] + "_") + "Event_" + requestParameters["eventID"] + ".csv"; - if (requestParameters["type"] == "pqds") - Filename = "PQDS_" + Filename; + string fileName = + $"{(requestParameters["type"] == "pqds" ? "PQDS_" : "")}" + + $"{(requestParameters["Meters"].Any() ? (requestParameters["Meter"].ToString() + "_") : "")}" + + $"{(requestParameters["EventType"].Any() ? (requestParameters["EventType"].ToString() + "_") : "")}" + + $"Event_{requestParameters["eventID"].ToString()}.csv"; - WriteTableToStream(requestParameters, stream, null, cancellationToken); - } - catch (Exception e) - { - LogExceptionHandler?.Invoke(e); - throw; - } - finally - { - stream.Close(); - } - }, - new MediaTypeHeaderValue(CsvContentType)); - - response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") + // No using block, asp.net takes care of it for us + MemoryStream stream = new MemoryStream(); + using (StreamWriter writer = new NoCloseStreamWriter(stream)) { - FileName = Filename - }; - - return Task.CompletedTask; + // todo: this can probably be turned async + WriteTableToStream(writer, requestParameters); + stream.Position = 0; + return new FileStreamResult(stream, "text/csv") + { + FileDownloadName = fileName + }; + } } - private void WriteTableToStream(NameValueCollection requestParameters, Stream responseStream, Action flushResponse, CompatibleCancellationToken cancellationToken) + private void WriteTableToStream(StreamWriter writer, IQueryCollection requestParameters) { - if (requestParameters["type"] == "csv") - ExportToCSV(responseStream, requestParameters); - else if (requestParameters["type"] == "pqds") - ExportToPQDS(responseStream, requestParameters); - else if (requestParameters["type"] == "stats") - ExportStatsToCSV(responseStream, requestParameters); - else if (requestParameters["type"] == "harmonics") - ExportHarmonicsToCSV(responseStream, requestParameters); - else if (requestParameters["type"] == "correlatedsags") - ExportCorrelatedSagsToCSV(responseStream, requestParameters); - else if (requestParameters["type"] == "fft") - ExportFFTToCSV(responseStream, requestParameters); + switch (requestParameters["type"].ToString()) + { + case "csv": + ExportToCSV(writer, requestParameters); + return; + case "pqds": + ExportToPQDS(writer, requestParameters); + return; + case "stats": + ExportStatsToCSV(writer, requestParameters); + return; + case "harmonics": + ExportHarmonicsToCSV(writer, requestParameters); + return; + case "correlatedsags": + ExportCorrelatedSagsToCSV(writer, requestParameters); + return; + case "fft": + ExportFFTToCSV(writer, requestParameters); + return; + default: + throw new ArgumentException("Parameter 'type' is not a valid value."); + } } // Converts the data group row of CSV data. private string ToCSV(IEnumerable data, int index) { - DateTime timestamp = this.m_epoch.Add(new TimeSpan((long)(data.First().DataPoints[index][1] * TimeSpan.TicksPerMillisecond))); + DateTime timestamp = m_epoch.Add(new TimeSpan((long)(data.First().DataPoints[index][1] * TimeSpan.TicksPerMillisecond))); IEnumerable row = new List() { timestamp.ToString("MM/dd/yyyy HH:mm:ss.fffffff"), timestamp.ToString("fffffff") }; @@ -198,40 +144,32 @@ private string GetCSVHeader(IEnumerable keys) return string.Join(",", headers); } - public void ExportToCSV(Stream returnStream, NameValueCollection requestParameters) + public void ExportToCSV(StreamWriter writer, IQueryCollection requestParameters) { IEnumerable data = BuildDataSeries(requestParameters); if (data.Count() == 0) return; - using (StreamWriter writer = new StreamWriter(returnStream)) - { - IEnumerable keys = data.Select(item => (item.LegendGroup + "-" + item.ChartLabel)) ; - // Write the CSV header to the file - writer.WriteLine(GetCSVHeader(keys)); + IEnumerable keys = data.Select(item => (item.LegendGroup + "-" + item.ChartLabel)) ; + // Write the CSV header to the file + writer.WriteLine(GetCSVHeader(keys)); - // Write data to the file - for (int i = 0; i < data.First().DataPoints.Count; ++i) - writer.WriteLine(ToCSV(data,i)); - } + // Write data to the file + for (int i = 0; i < data.First().DataPoints.Count; ++i) + writer.WriteLine(ToCSV(data, i)); } - - public void ExportToPQDS(Stream returnStream, NameValueCollection requestParameters) + public void ExportToPQDS(StreamWriter writer, IQueryCollection requestParameters) { - int eventID = int.Parse(requestParameters["eventID"]); + int eventID = int.Parse(requestParameters["eventID"].ToString()); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Event evt = (new TableOperations(connection)).QueryRecordWhere("ID = {0}", eventID); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); - // only get Single Voltage and Single Current Data for This.... List data = new List(); List metaData = new List(); VIDataGroup dataGroup = OpenSEEBaseController - .QueryVIDataGroupAsync(evt.ID, meter) + .QueryVIDataGroupAsync(eventID, connection) .GetAwaiter() .GetResult(); @@ -255,22 +193,22 @@ public void ExportToPQDS(Stream returnStream, NameValueCollection requestParamet return; // Add MetaData Information - metaData = PQDSMetaData(evt, meter); + Event evt = (new TableOperations(connection)).QueryRecordWhere("ID = {0}", eventID); + metaData = PQDSMetaData(evt, connection); PQDS.PQDSFile file = new PQDS.PQDSFile(metaData, data, evt.StartTime); - using (StreamWriter writer = new StreamWriter(returnStream)) - { - file.WriteToStream(writer); - } + file.WriteToStream(writer); } } - private List PQDSMetaData(Event evt, Meter meter) + private List PQDSMetaData(Event evt, AdoDataConnection connection) { List result = new List(); + Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); + Asset asset = (new TableOperations(connection)).QueryRecordWhere("ID = {0}", evt.AssetID); + meter.ConnectionFactory = () => new AdoDataConnection(Settings.Default); - result.Add(new PQDS.MetaDataTag("DeviceName", meter.Name)); result.Add(new PQDS.MetaDataTag("DeviceAlias", meter.ShortName)); result.Add(new PQDS.MetaDataTag("DeviceLocation", meter.Location.Name)); @@ -278,95 +216,80 @@ public void ExportToPQDS(Stream returnStream, NameValueCollection requestParamet result.Add(new PQDS.MetaDataTag("Latitude", Convert.ToString(meter.Location.Latitude))); result.Add(new PQDS.MetaDataTag("Longitude", Convert.ToString(meter.Location.Longitude))); - Asset asset; - double systemFrequency; - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) - { - asset = (new TableOperations(connection)).QueryRecordWhere("ID = {0}", evt.AssetID); - systemFrequency = connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'SystemFrequency'") ?? 60.0; + double systemFrequency = connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'SystemFrequency'") ?? 60.0; - if (asset != null) - { - result.Add(new PQDS.MetaDataTag("NominalVoltage-LG", asset.VoltageKV)); - result.Add(new PQDS.MetaDataTag("NominalFrequency", systemFrequency)); - result.Add(new PQDS.MetaDataTag("AssetName", asset.AssetKey)); + if (asset != null) + { + result.Add(new PQDS.MetaDataTag("NominalVoltage-LG", asset.VoltageKV)); + result.Add(new PQDS.MetaDataTag("NominalFrequency", systemFrequency)); + result.Add(new PQDS.MetaDataTag("AssetName", asset.AssetKey)); - if (asset.AssetTypeID == connection.ExecuteScalar("SELECT ID FROM AssetType WHERE Name = 'Line'")) - result.Add(new PQDS.MetaDataTag("LineLength", connection.ExecuteScalar("SELECT Length FROM LineView WHERE ID = {0}", asset.ID))); - } + if (asset.AssetTypeID == connection.ExecuteScalar("SELECT ID FROM AssetType WHERE Name = 'Line'")) + result.Add(new PQDS.MetaDataTag("LineLength", connection.ExecuteScalar("SELECT Length FROM LineView WHERE ID = {0}", asset.ID))); + } - result.Add(new PQDS.MetaDataTag("EventID", evt.Name)); - result.Add(new PQDS.MetaDataTag("EventGUID", Guid.NewGuid().ToString())); - result.Add(new PQDS.MetaDataTag("EventDuration", (evt.EndTime - evt.StartTime).TotalMilliseconds)); - result.Add(new PQDS.MetaDataTag("EventTypeCode", PQDSEventTypeCode(evt.EventTypeID))); + result.Add(new PQDS.MetaDataTag("EventID", evt.Name)); + result.Add(new PQDS.MetaDataTag("EventGUID", Guid.NewGuid().ToString())); + result.Add(new PQDS.MetaDataTag("EventDuration", (evt.EndTime - evt.StartTime).TotalMilliseconds)); + result.Add(new PQDS.MetaDataTag("EventTypeCode", PQDSEventTypeCode(evt.EventTypeID))); - EventStat stat = (new TableOperations(connection)).QueryRecordWhere("EventID = {0}", evt.ID); + EventStat stat = (new TableOperations(connection)).QueryRecordWhere("EventID = {0}", evt.ID); - if (stat != null) + if (stat != null) + { + if (stat.VAMax != null) { - if (stat.VAMax != null) - { - result.Add(new PQDS.MetaDataTag("EventMaxVA", (double)stat.VAMax)); - } - if (stat.VBMax != null) - { - result.Add(new PQDS.MetaDataTag("EventMaxVB", (double)stat.VBMax)); - } - if (stat.VCMax != null) - { - result.Add(new PQDS.MetaDataTag("EventMaxVC", (double)stat.VCMax)); - } - if (stat.VAMin != null) - { - result.Add(new PQDS.MetaDataTag("EventMinVA", (double)stat.VAMin)); - } - if (stat.VBMin != null) - { - result.Add(new PQDS.MetaDataTag("EventMinVB", (double)stat.VBMin)); - } - if (stat.VCMin != null) - { - result.Add(new PQDS.MetaDataTag("EventMinVC", (double)stat.VCMin)); - } - - if (stat.IAMax != null) - { - result.Add(new PQDS.MetaDataTag("EventMaxIA", (double)stat.IAMax)); - } - if (stat.IBMax != null) - { - result.Add(new PQDS.MetaDataTag("EventMaxIB", (double)stat.IBMax)); - } - if (stat.ICMax != null) - { - result.Add(new PQDS.MetaDataTag("EventMaxIC", (double)stat.ICMax)); - } - - + result.Add(new PQDS.MetaDataTag("EventMaxVA", (double)stat.VAMax)); + } + if (stat.VBMax != null) + { + result.Add(new PQDS.MetaDataTag("EventMaxVB", (double)stat.VBMax)); + } + if (stat.VCMax != null) + { + result.Add(new PQDS.MetaDataTag("EventMaxVC", (double)stat.VCMax)); + } + if (stat.VAMin != null) + { + result.Add(new PQDS.MetaDataTag("EventMinVA", (double)stat.VAMin)); + } + if (stat.VBMin != null) + { + result.Add(new PQDS.MetaDataTag("EventMinVB", (double)stat.VBMin)); + } + if (stat.VCMin != null) + { + result.Add(new PQDS.MetaDataTag("EventMinVC", (double)stat.VCMin)); } - result.Add(new PQDS.MetaDataTag("EventYear", ((DateTime)evt.StartTime).Year)); - - result.Add(new PQDS.MetaDataTag("EventMonth", (evt.StartTime).Month)); - result.Add(new PQDS.MetaDataTag("EventDay", (evt.StartTime).Day)); - result.Add(new PQDS.MetaDataTag("EventHour", (evt.StartTime).Hour)); - result.Add(new PQDS.MetaDataTag("EventMinute", (evt.StartTime).Minute)); - result.Add(new PQDS.MetaDataTag("EventSecond", (evt.StartTime).Second)); - result.Add(new PQDS.MetaDataTag("EventNanoSecond", Get_nanoseconds(evt.StartTime))); - - String date = String.Format("{0:D2}/{1:D2}/{2:D4}", (evt.StartTime).Month, (evt.StartTime).Day, (evt.StartTime).Year); - String time = String.Format("{0:D2}:{1:D2}:{2:D2}", (evt.StartTime).Hour, (evt.StartTime).Minute, (evt.StartTime).Second); - result.Add(new PQDS.MetaDataTag("EventDate", date)); - result.Add(new PQDS.MetaDataTag("EventTime", time)); - - + if (stat.IAMax != null) + { + result.Add(new PQDS.MetaDataTag("EventMaxIA", (double)stat.IAMax)); + } + if (stat.IBMax != null) + { + result.Add(new PQDS.MetaDataTag("EventMaxIB", (double)stat.IBMax)); + } + if (stat.ICMax != null) + { + result.Add(new PQDS.MetaDataTag("EventMaxIC", (double)stat.ICMax)); + } } - - + result.Add(new PQDS.MetaDataTag("EventYear", ((DateTime)evt.StartTime).Year)); + result.Add(new PQDS.MetaDataTag("EventMonth", (evt.StartTime).Month)); + result.Add(new PQDS.MetaDataTag("EventDay", (evt.StartTime).Day)); + result.Add(new PQDS.MetaDataTag("EventHour", (evt.StartTime).Hour)); + result.Add(new PQDS.MetaDataTag("EventMinute", (evt.StartTime).Minute)); + result.Add(new PQDS.MetaDataTag("EventSecond", (evt.StartTime).Second)); + result.Add(new PQDS.MetaDataTag("EventNanoSecond", Get_nanoseconds(evt.StartTime))); + + String date = String.Format("{0:D2}/{1:D2}/{2:D4}", (evt.StartTime).Month, (evt.StartTime).Day, (evt.StartTime).Year); + String time = String.Format("{0:D2}:{1:D2}:{2:D2}", (evt.StartTime).Hour, (evt.StartTime).Minute, (evt.StartTime).Second); + result.Add(new PQDS.MetaDataTag("EventDate", date)); + result.Add(new PQDS.MetaDataTag("EventTime", time)); return result; - } private int Get_nanoseconds(DateTime date) @@ -376,15 +299,11 @@ private int Get_nanoseconds(DateTime date) result = result - (long)day.Hours * (60L * 60L * 10000000L); result = result - (long)day.Minutes * (60L * 10000000L); result = result - (long)day.Seconds * 10000000L; - - return ((int)result * 100); } - private int PQDSEventTypeCode(int XDAevtTypeID) - { - return 0; - } + private int PQDSEventTypeCode(int XDAevtTypeID) => 0; + private PQDS.DataSeries PQDSSeries(DataSeries data, string label) { PQDS.DataSeries result = new PQDS.DataSeries(label); @@ -394,11 +313,10 @@ private PQDS.DataSeries PQDSSeries(DataSeries data, string label) return result; } - public void ExportStatsToCSV(Stream returnStream, NameValueCollection requestParameters) + public void ExportStatsToCSV(StreamWriter writer, IQueryCollection requestParameters) { - int eventId = int.Parse(requestParameters["eventId"]); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) - using (StreamWriter writer = new StreamWriter(returnStream)) + int eventId = int.Parse(requestParameters["eventId"].ToString()); + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { DataTable dataTable = connection.RetrieveData("SELECT * FROM OpenSEEScalarStatView WHERE EventID = {0}", eventId); DataRow row = dataTable.AsEnumerable().First(); @@ -412,12 +330,11 @@ public void ExportStatsToCSV(Stream returnStream, NameValueCollection requestPar } } - public void ExportCorrelatedSagsToCSV(Stream returnStream, NameValueCollection requestParameters) + public void ExportCorrelatedSagsToCSV(StreamWriter writer, IQueryCollection requestParameters) { - int eventID = int.Parse(requestParameters["eventId"]); + int eventID = int.Parse(requestParameters["eventId"].ToString()); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) - using (StreamWriter writer = new StreamWriter(returnStream)) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { double timeTolerance = connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'TimeTolerance'"); DateTime startTime = connection.ExecuteScalar("SELECT StartTime FROM Event WHERE ID = {0}", eventID); @@ -450,24 +367,19 @@ private class PhasorResult { public double Angle; } - public void ExportFFTToCSV(Stream returnStream, NameValueCollection requestParameters) + public void ExportFFTToCSV(StreamWriter writer, IQueryCollection requestParameters) { - int eventId = int.Parse(requestParameters["eventID"]); + int eventId = int.Parse(requestParameters["eventID"].ToString()); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) - using (StreamWriter writer = new StreamWriter(returnStream)) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - double startTime = requestParameters["startDate"] == null ? 0.0 : double.Parse(requestParameters["startDate"]); - int cycles = requestParameters["cycles"] == null ? 0 : int.Parse(requestParameters["cycles"]); - - Event evt = (new TableOperations(connection)).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); + double startTime = requestParameters["startDate"].Any() ? double.Parse(requestParameters["startDate"].ToString()) : 0.0; + int cycles = requestParameters["cycles"].Any() ? int.Parse(requestParameters["cycles"].ToString()) : 0; AnalyticController ctrl = new AnalyticController(); DataGroup dataGroup = OpenSEEBaseController - .QueryDataGroupAsync(evt.ID, meter) + .QueryDataGroupAsync(eventId, connection) .GetAwaiter() .GetResult(); @@ -490,11 +402,10 @@ public void ExportFFTToCSV(Stream returnStream, NameValueCollection requestParam } } - public void ExportHarmonicsToCSV(Stream returnStream, NameValueCollection requestParameters) + public void ExportHarmonicsToCSV(StreamWriter writer, IQueryCollection requestParameters) { - int eventId = int.Parse(requestParameters["eventId"]); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) - using (StreamWriter writer = new StreamWriter(returnStream)) + int eventId = int.Parse(requestParameters["eventId"].ToString()); + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { DataTable dataTable = connection.RetrieveData(@" SELECT @@ -543,46 +454,60 @@ SnapshotHarmonics JOIN } } - public List BuildDataSeries(NameValueCollection requestParameters) + public List BuildDataSeries(IQueryCollection requestParameters) { - int eventID = int.Parse(requestParameters["eventID"]); - bool displayVolt = requestParameters["displayVolt"] == null ? true: bool.Parse(requestParameters["displayVolt"]); - bool displayCur = requestParameters["displayCur"] == null ? true : bool.Parse(requestParameters["displayCur"]); - bool displayTCE = requestParameters["displayTCE"] == null ? false : bool.Parse(requestParameters["displayTCE"]); - bool breakerdigitals = requestParameters["breakerdigitals"] == null ? false : bool.Parse(requestParameters["breakerdigitals"]); - bool displayAnalogs = requestParameters["displayAnalogs"] == null ? false : bool.Parse(requestParameters["displayAnalogs"]); - int lpfOrder = requestParameters["lpfOrder"] == null ? 0 : int.Parse(requestParameters["lpfOrder"]); - int hpfOrder = requestParameters["hpfOrder"] == null ? 0 : int.Parse(requestParameters["hpfOrder"]); - double Trc = requestParameters["Trc"] == null ? 0.0 : double.Parse(requestParameters["Trc"]); - int harmonic = requestParameters["harmonic"] == null ? 1 : int.Parse(requestParameters["harmonic"]); - List displayAnalytics = requestParameters["displayAnalytics"] == null ? new List() : requestParameters["displayAnalytics"].Split(',').ToList(); + int eventID = int.Parse(requestParameters["eventID"].ToString()); + bool displayVolt = requestParameters["displayVolt"].Any() ? bool.Parse(requestParameters["displayVolt"]) : true; + bool displayCur = requestParameters["displayCur"].Any() ? bool.Parse(requestParameters["displayCur"]) : true; + bool displayTCE = requestParameters["displayTCE"].Any() ? bool.Parse(requestParameters["displayTCE"]) : false; + bool breakerdigitals = requestParameters["breakerdigitals"].Any() ? bool.Parse(requestParameters["breakerdigitals"]) : false; + bool displayAnalogs = requestParameters["displayAnalogs"].Any() ? bool.Parse(requestParameters["displayAnalogs"]) : false; + int lpfOrder = requestParameters["lpfOrder"].Any() ? int.Parse(requestParameters["lpfOrder"]) : 0; + int hpfOrder = requestParameters["hpfOrder"].Any() ? int.Parse(requestParameters["hpfOrder"]) : 0; + double Trc = requestParameters["Trc"].Any() ? double.Parse(requestParameters["Trc"]) : 0.0; + int harmonic = requestParameters["harmonic"].Any() ? int.Parse(requestParameters["harmonic"]) : 1; + List displayAnalytics = requestParameters["displayAnalytics"].Any() ? requestParameters["displayAnalytics"].ToString().Split(',').ToList() : new List(); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Event evt = (new TableOperations(connection)).QueryRecordWhere("ID = {0}", eventID); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); + DataGroup dataGroup = OpenSEEBaseController + .QueryDataGroupAsync(eventID, connection) + .GetAwaiter() + .GetResult(); + + VICycleDataGroup viCycleDataGroup = OpenSEEBaseController + .QueryVICycleDataGroupAsync(eventID, connection) + .GetAwaiter() + .GetResult(); + + Lazy lazyVIDataGroup = new Lazy(() => + { + return OpenSEEBaseController + .QueryVIDataGroupAsync(eventID, connection) + .GetAwaiter() + .GetResult(); + }); IEnumerable returnList = new List(); if (displayVolt) - returnList = returnList.Concat(QueryVoltageData(meter, evt)); + returnList = returnList.Concat(QueryVoltageData(dataGroup, viCycleDataGroup)); if(displayCur) - returnList = returnList.Concat(QueryCurrentData(meter, evt)); + returnList = returnList.Concat(QueryCurrentData(dataGroup, viCycleDataGroup)); if(displayTCE) - returnList = returnList.Concat(QueryTCEData(meter, evt)); + returnList = returnList.Concat(QueryTCEData(dataGroup)); if (displayAnalogs) - returnList = returnList.Concat(QueryAnalogData(meter, evt)); + returnList = returnList.Concat(QueryAnalogData(dataGroup)); if (breakerdigitals) - returnList = returnList.Concat(QueryDigitalData(meter, evt)); + returnList = returnList.Concat(QueryDigitalData(dataGroup)); foreach (var analytics in displayAnalytics) { if (!string.IsNullOrEmpty(analytics)) - returnList = returnList.Concat(QueryAnalyticData(meter, evt, analytics, lpfOrder, hpfOrder, Trc, harmonic)); + returnList = returnList.Concat(QueryAnalyticData(dataGroup, viCycleDataGroup, lazyVIDataGroup, analytics, lpfOrder, hpfOrder, Trc, harmonic)); } returnList = AlignData(returnList.ToList()); @@ -591,86 +516,58 @@ public List BuildDataSeries(NameValueCollection requestParameters) } } - private List QueryAnalyticData(Meter meter, Event evt, string analytic, int lowPassOrder, int highPassOrder, double Trc, int harmonic) + private List QueryAnalyticData(DataGroup dataGroup, VICycleDataGroup viCycleData, Lazy lazyVIDataGroup, string analytic, int lowPassOrder, int highPassOrder, double Trc, int harmonic) { - Lazy lazyDataGroup = new Lazy(() => - { - return OpenSEEBaseController - .QueryDataGroupAsync(evt.ID, meter) - .GetAwaiter() - .GetResult(); - }); - - Lazy lazyVIDataGroup = new Lazy(() => - { - return OpenSEEBaseController - .QueryVIDataGroupAsync(evt.ID, meter) - .GetAwaiter() - .GetResult(); - }); - - Lazy lazyVICycleDataGroup = new Lazy(() => - { - return OpenSEEBaseController - .QueryVICycleDataGroupAsync(evt.ID, meter) - .GetAwaiter() - .GetResult(); - }); AnalyticController controller = new AnalyticController(); if (analytic == "FirstDerivative") - return controller.GetFirstDerivativeLookup(lazyDataGroup.Value, lazyVICycleDataGroup.Value); + return controller.GetFirstDerivativeLookup(dataGroup, viCycleData); if (analytic == "ClippedWaveforms") - return controller.GetClippedWaveformsLookup(lazyDataGroup.Value); + return controller.GetClippedWaveformsLookup(dataGroup); if (analytic == "Frequency") return controller.GetFrequencyLookup(lazyVIDataGroup.Value); if (analytic == "Impedance") - return controller.GetImpedanceLookup(lazyVICycleDataGroup.Value); + return controller.GetImpedanceLookup(viCycleData); if (analytic == "Power") - return controller.GetPowerLookup(lazyVICycleDataGroup.Value); + return controller.GetPowerLookup(viCycleData); if (analytic == "RemoveCurrent") - return controller.GetRemoveCurrentLookup(lazyDataGroup.Value); + return controller.GetRemoveCurrentLookup(dataGroup); if (analytic == "MissingVoltage") - return controller.GetMissingVoltageLookup(lazyDataGroup.Value); + return controller.GetMissingVoltageLookup(dataGroup); if (analytic == "LowPassFilter") - return controller.GetLowPassFilterLookup(lazyDataGroup.Value, lowPassOrder); + return controller.GetLowPassFilterLookup(dataGroup, lowPassOrder); if (analytic == "HighPassFilter") - return controller.GetHighPassFilterLookup(lazyDataGroup.Value, highPassOrder); + return controller.GetHighPassFilterLookup(dataGroup, highPassOrder); if (analytic == "SymmetricalComponents") - return controller.GetSymmetricalComponentsLookup(lazyVICycleDataGroup.Value); + return controller.GetSymmetricalComponentsLookup(viCycleData); if (analytic == "Unbalance") - return controller.GetUnbalanceLookup(lazyVICycleDataGroup.Value); + return controller.GetUnbalanceLookup(viCycleData); if (analytic == "Rectifier") return controller.GetRectifierLookup(lazyVIDataGroup.Value, Trc); if (analytic == "RapidVoltageChange") - return controller.GetRapidVoltageChangeLookup(lazyVICycleDataGroup.Value); + return controller.GetRapidVoltageChangeLookup(viCycleData); if (analytic == "THD") - return controller.GetTHDLookup(lazyDataGroup.Value, true); + return controller.GetTHDLookup(dataGroup, true); if (analytic == "SpecifiedHarmonic") - return controller.GetSpecifiedHarmonicLookup(lazyDataGroup.Value, harmonic, true); + return controller.GetSpecifiedHarmonicLookup(dataGroup, harmonic, true); if (analytic == "OverlappingWaveform") - return controller.GetOverlappingWaveformLookup(lazyDataGroup.Value); + return controller.GetOverlappingWaveformLookup(dataGroup); return new List(); } - private List QueryVoltageData(Meter meter, Event evt) + private List QueryVoltageData(DataGroup dataGroup, VICycleDataGroup viCycleDataGroup) { bool useLL; - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - useLL = connection.ExecuteScalar("SELECT Value FROM Settings WHERE Name = 'useLLVoltage'") ?? false; + useLL = bool.Parse(new TableOperations(connection).QueryRecordWhere("Name = {0}", "useLLVoltage")?.Value ?? bool.FalseString); } - DataGroup dataGroup = OpenSEEBaseController - .QueryDataGroupAsync(evt.ID, meter) - .GetAwaiter() - .GetResult(); - List WaveForm = dataGroup.DataSeries.Where(ds => ds.SeriesInfo.Channel.MeasurementType.Name == "Voltage" && ( - (useLL && !(ds.SeriesInfo.Channel.Phase.Name == "AB" || ds.SeriesInfo.Channel.Phase.Name == "BC" || ds.SeriesInfo.Channel.Phase.Name == "CA")) || - (!useLL && (ds.SeriesInfo.Channel.Phase.Name == "AB" || ds.SeriesInfo.Channel.Phase.Name == "BC" || ds.SeriesInfo.Channel.Phase.Name == "CA"))) + (useLL && (ds.SeriesInfo.Channel.Phase.Name == "AB" || ds.SeriesInfo.Channel.Phase.Name == "BC" || ds.SeriesInfo.Channel.Phase.Name == "CA")) || + (!useLL && !(ds.SeriesInfo.Channel.Phase.Name == "AB" || ds.SeriesInfo.Channel.Phase.Name == "BC" || ds.SeriesInfo.Channel.Phase.Name == "CA"))) ).Select( ds => new D3Series() { @@ -688,11 +585,6 @@ private List QueryVoltageData(Meter meter, Event evt) return a.LegendGroup.CompareTo(b.LegendGroup); }); - VICycleDataGroup viCycleDataGroup = OpenSEEBaseController - .QueryVICycleDataGroupAsync(evt.ID, meter) - .GetAwaiter() - .GetResult(); - List result = new List(); foreach(D3Series w in WaveForm) @@ -724,12 +616,8 @@ private List QueryVoltageData(Meter meter, Event evt) return result; } - private List QueryCurrentData(Meter meter, Event evt) + private List QueryCurrentData(DataGroup dataGroup, VICycleDataGroup viCycleDataGroup) { - DataGroup dataGroup = OpenSEEBaseController - .QueryDataGroupAsync(evt.ID, meter) - .GetAwaiter() - .GetResult(); List WaveForm = dataGroup.DataSeries.Where(ds => ds.SeriesInfo.Channel.MeasurementType.Name == "Current" ).Select( @@ -749,11 +637,6 @@ private List QueryCurrentData(Meter meter, Event evt) return a.LegendGroup.CompareTo(b.LegendGroup); }); - VICycleDataGroup viCycleDataGroup = OpenSEEBaseController - .QueryVICycleDataGroupAsync(evt.ID, meter) - .GetAwaiter() - .GetResult(); - List result = new List(); foreach (D3Series w in WaveForm) @@ -785,44 +668,34 @@ private List QueryCurrentData(Meter meter, Event evt) return result; } - private List QueryTCEData(Meter meter, Event evt) + private List QueryTCEData(DataGroup dataGroup) { - DataGroup dataGroup = OpenSEEBaseController - .QueryDataGroupAsync(evt.ID, meter) - .GetAwaiter() - .GetResult(); - - List result = dataGroup.DataSeries.Where(ds => ds.SeriesInfo.Channel.MeasurementType.Name == "TripCoilCurrent" - ).Select( + return dataGroup.DataSeries + .Where(ds => ds.SeriesInfo.Channel.MeasurementType.Name == "TripCoilCurrent") + .Select( ds => new D3Series() { ChannelID = ds.SeriesInfo.Channel.ID, ChartLabel = OpenSEEBaseController.GetChartLabel(ds.SeriesInfo.Channel), LegendGroup = ds.SeriesInfo.Channel.Asset.AssetName, DataPoints = ds.DataPoints.Select(dataPoint => new double[] { dataPoint.Time.Subtract(m_epoch).TotalMilliseconds, dataPoint.Value }).ToList(), - }).ToList(); - - return result; + }) + .ToList(); } - private List QueryDigitalData(Meter meter, Event evt) + private List QueryDigitalData(DataGroup dataGroup) { - DataGroup dataGroup = OpenSEEBaseController - .QueryDataGroupAsync(evt.ID, meter) - .GetAwaiter() - .GetResult(); - - List result = dataGroup.DataSeries.Where(ds => ds.SeriesInfo.Channel.MeasurementType.Name == "Digital" - ).Select( + return dataGroup.DataSeries + .Where(ds => ds.SeriesInfo.Channel.MeasurementType.Name == "Digital") + .Select( ds => new D3Series() { ChannelID = ds.SeriesInfo.Channel.ID, ChartLabel = OpenSEEController.GetChartLabel(ds.SeriesInfo.Channel), LegendGroup = ds.SeriesInfo.Channel.Asset.AssetName, DataPoints = ds.DataPoints.Select(dataPoint => new double[] { dataPoint.Time.Subtract(m_epoch).TotalMilliseconds, dataPoint.Value }).ToList(), - }).ToList(); - - return result; + }) + .ToList(); } private List AlignData(List data) @@ -854,40 +727,27 @@ private List AlignData(List data) return item; }); - return result.ToList(); - } - private List QueryAnalogData(Meter meter, Event evt) + private List QueryAnalogData(DataGroup dataGroup) { - DataGroup dataGroup = OpenSEEBaseController - .QueryDataGroupAsync(evt.ID, meter) - .GetAwaiter() - .GetResult(); - - List dataLookup = dataGroup.DataSeries.Where(ds => - ds.SeriesInfo.Channel.MeasurementType.Name != "Digital" && - ds.SeriesInfo.Channel.MeasurementType.Name != "Voltage" && - ds.SeriesInfo.Channel.MeasurementType.Name != "Current" && - ds.SeriesInfo.Channel.MeasurementType.Name != "TripCoilCurrent").Select(ds => - new D3Series() - { - ChannelID = ds.SeriesInfo.Channel.ID, - ChartLabel = ds.SeriesInfo.Channel.Description ?? OpenSEEBaseController.GetChartLabel(ds.SeriesInfo.Channel), - LegendGroup = ds.SeriesInfo.Channel.Asset.AssetName, - DataPoints = ds.DataPoints.Select(dataPoint => new double[] { dataPoint.Time.Subtract(m_epoch).TotalMilliseconds, dataPoint.Value }).ToList(), - }).ToList(); - - return dataLookup; + return dataGroup.DataSeries + .Where(ds => + ds.SeriesInfo.Channel.MeasurementType.Name != "Digital" && + ds.SeriesInfo.Channel.MeasurementType.Name != "Voltage" && + ds.SeriesInfo.Channel.MeasurementType.Name != "Current" && + ds.SeriesInfo.Channel.MeasurementType.Name != "TripCoilCurrent").Select(ds => + new D3Series() + { + ChannelID = ds.SeriesInfo.Channel.ID, + ChartLabel = ds.SeriesInfo.Channel.Description ?? OpenSEEBaseController.GetChartLabel(ds.SeriesInfo.Channel), + LegendGroup = ds.SeriesInfo.Channel.Asset.AssetName, + DataPoints = ds.DataPoints.Select(dataPoint => new double[] { dataPoint.Time.Subtract(m_epoch).TotalMilliseconds, dataPoint.Value }).ToList(), + } + ).ToList(); } #endregion - - #region [ Static ] - - public static Action LogExceptionHandler; - - #endregion } -} \ No newline at end of file +} diff --git a/src/OpenSEE/Controllers/FaultSpecificsController.cs b/src/OpenSEE/Controllers/FaultSpecificsController.cs new file mode 100644 index 00000000..6616fc3f --- /dev/null +++ b/src/OpenSEE/Controllers/FaultSpecificsController.cs @@ -0,0 +1,35 @@ +//****************************************************************************************************** +// FaultSpecificsController.cs - Gbtc +// +// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/05/2026 - Gabriel Santos +// Refactored code to be in controller format for netcore upgrade. +// +//****************************************************************************************************** + +using Gemstone.Web.APIController; +using Microsoft.AspNetCore.Mvc; +using OpenSEE.Models; + +namespace OpenSEE +{ + /// + /// Controller to fetch FaultSpecifics for faults. + /// + [Route("api/openSEE/FaultSpecifics")] + public class FaultSpecificsController : ReadOnlyModelController { } +} \ No newline at end of file diff --git a/src/OpenSEE/Controllers/HomeController.cs b/src/OpenSEE/Controllers/HomeController.cs index 4994bd26..1c3e7858 100644 --- a/src/OpenSEE/Controllers/HomeController.cs +++ b/src/OpenSEE/Controllers/HomeController.cs @@ -21,13 +21,11 @@ // //****************************************************************************************************** -using System; -using System.Web.Mvc; -using GSF.Data; -using GSF.Data.Model; -using GSF.Identity; -using GSF.Web.Model; -using OpenSEE.Model; +using Gemstone.Configuration; +using Gemstone.Data; +using Gemstone.Data.Model; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using openXDA.Model; namespace OpenSEE.Controllers @@ -35,47 +33,27 @@ namespace OpenSEE.Controllers /// /// Represents a MVC controller for the site's main pages. /// + [Authorize] public class HomeController : Controller { - #region [ Constructors ] - - public HomeController() - { - ViewData.Model = new AppModel(); - } - - #endregion - - #region [ Methods ] - - public ActionResult Home() + // This provides some default values for if nothing is supplied in the query string + public IActionResult Index() { - ViewBag.IsAdmin = User.IsInRole("Administrator"); - int eventID = -1; Event evt; - if (Request.QueryString.Get("eventid") != null) - eventID = int.Parse(Request.QueryString["eventid"]); - - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { TableOperations eventTable = new TableOperations(connection); - if (eventID == -1) - eventID = eventTable.QueryRecord("ID > 0").ID; - + eventID = eventTable.QueryRecord("ID > 0").ID; evt = eventTable.QueryRecordWhere("ID = {0}", eventID); - } - ViewBag.EventID = eventID; - ViewBag.EventStartTime = evt.StartTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffff"); - ViewBag.EventEndTime = evt.EndTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffff"); - ViewBag.SamplesPerCycle = evt.SamplesPerCycle; - ViewBag.Cycles = Math.Floor((evt.EndTime - evt.StartTime).TotalSeconds * 60.0D); - return View("Index"); + ViewBag.DefaultEventID = eventID; + ViewBag.DefaultEventStartTime = evt.StartTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffff"); + ViewBag.DefaultEventEndTime = evt.EndTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffff"); + return View("Index"); + } } - - #endregion } } \ No newline at end of file diff --git a/src/OpenSEE/Controllers/OpenSEEController.cs b/src/OpenSEE/Controllers/OpenSEEController.cs index 926e20eb..46f63c08 100644 --- a/src/OpenSEE/Controllers/OpenSEEController.cs +++ b/src/OpenSEE/Controllers/OpenSEEController.cs @@ -28,26 +28,28 @@ using System; using System.Collections.Generic; using System.Data; -using System.Data.SqlClient; using System.Globalization; using System.Linq; using System.Runtime.Caching; using System.Threading.Tasks; -using System.Web.Http; using FaultData.DataAnalysis; -using GSF.Data; -using GSF.Data.Model; -using GSF.Web; +using Gemstone.Configuration; +using Gemstone.Data; +using Gemstone.Data.DataExtensions; +using Gemstone.Data.Model; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Primitives; using OpenSEE.Model; using openXDA.Model; namespace OpenSEE { - [RoutePrefix("api/OpenSEE")] + [Route("api/OpenSEE")] public class OpenSEEController : OpenSEEBaseController { #region [ Members ] - + // Constants public const string TimeCorrelatedSagsSQL = "SELECT Disturbance.* " + @@ -100,13 +102,13 @@ public OpenSEEController() : base() { } #endregion #region [ Static ] - + static OpenSEEController() { s_memoryCache = new MemoryCache("openSEE"); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { m_cacheSlidingExpiration = connection.ExecuteScalar("SELECT Value FROM [OpenSee.Setting] WHERE Name = 'SlidingCacheExpiration'") ?? 2.0; } @@ -117,41 +119,37 @@ static OpenSEEController() #region [ Waveform Data ] - [Route("GetData"),HttpGet] + [Route("GetData"), HttpGet] public async Task GetData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - - int eventId = int.Parse(query["eventId"]); - string type = query["type"]; - string dataType = query["dataType"]; + int eventId = int.Parse(Request.Query["eventId"].ToString()); + string type = Request.Query["type"].ToString(); + string dataType = Request.Query["dataType"].ToString(); bool forceFullRes = - query.TryGetValue("fullRes", out string fullResSetting) && - int.TryParse(fullResSetting, out int fullResNum) && + Request.Query.TryGetValue("fullRes", out StringValues fullResSetting) && + int.TryParse(fullResSetting.ToString(), out int fullResNum) && fullResNum != 0; bool dbgNocompress = - query.TryGetValue("dbgNocompress", out string dbgNocompressSetting) && - int.TryParse(dbgNocompressSetting, out int dbgNocompressNum) && + Request.Query.TryGetValue("dbgNocompress", out StringValues dbgNocompressSetting) && + int.TryParse(dbgNocompressSetting.ToString(), out int dbgNocompressNum) && dbgNocompressNum != 0; Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection("systemSettings"); List returnList = new List(); if (dataType == "Time") { - DataGroup dataGroup = await QueryDataGroupAsync(eventId, meter); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); returnList = GetD3DataLookup(dataGroup, type, evt.ID); } else { - VICycleDataGroup viCycleDataGroup = await QueryVICycleDataGroupAsync(eventId, meter, !dbgNocompress); + VICycleDataGroup viCycleDataGroup = await QueryVICycleDataGroupAsync(eventId, connection, !dbgNocompress); returnList = GetD3FrequencyDataLookup(viCycleDataGroup, type, !forceFullRes); } @@ -166,24 +164,26 @@ public async Task GetData() DownSample(returnDict); return returnDict; - } + } } private List GetD3DataLookup(DataGroup dataGroup, string type, int evtID) { List dataLookup; - + dataLookup = dataGroup.DataSeries.Where(ds => ds.SeriesInfo.Channel.MeasurementType.Name == type).Select( - ds => { + ds => + { if (type == "TripCoilCurrent") { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { RelayPerformance relayPerformance = new TableOperations(connection).QueryRecordWhere("EventID = {0} AND ChannelID = {1}", evtID, ds.SeriesInfo.ChannelID); List dataMarkers = new List(); - - if (relayPerformance != null) { + + if (relayPerformance != null) + { try { @@ -238,7 +238,8 @@ private List GetD3DataLookup(DataGroup dataGroup, string type, int evt }; } } - else { + else + { return new D3Series() { LegendVGroup = GetVoltageType(ds.SeriesInfo.Channel), @@ -252,7 +253,7 @@ private List GetD3DataLookup(DataGroup dataGroup, string type, int evt DataMarker = new List(), BaseValue = (type == "Voltage" ? GetBaseV(ds.SeriesInfo.Channel, false) * 1000.0 : GetIbase(Sbase, ds.SeriesInfo.Channel.Asset.VoltageKV)) }; - } + } }).ToList(); if (type == "TripCoilCurrent") @@ -282,7 +283,7 @@ private List GetD3FrequencyDataLookup(VICycleDataGroup vICycleDataGrou { //Determine Sbase double Sbase = 0; - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) Sbase = connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'SystemMVABase'"); IEnumerable names = vICycleDataGroup.CycleDataGroups.Where(ds => ds.RMS.SeriesInfo.Channel.MeasurementType.Name == type).Select(ds => ds.RMS.SeriesInfo.Channel.Phase.Name); @@ -290,7 +291,7 @@ private List GetD3FrequencyDataLookup(VICycleDataGroup vICycleDataGrou foreach (CycleDataGroup cdg in vICycleDataGroup.CycleDataGroups.Where(ds => ds.RMS.SeriesInfo.Channel.MeasurementType.Name == type)) { - + D3Series flotSeriesRMS = new D3Series { LegendHorizontal = "RMS", @@ -332,9 +333,9 @@ private List GetD3FrequencyDataLookup(VICycleDataGroup vICycleDataGrou Color = GetColor(cdg.Phase.SeriesInfo.Channel), LegendVGroup = GetVoltageType(cdg.Phase.SeriesInfo.Channel), LegendGroup = cdg.Asset.AssetName, - BaseValue = 1.0, + BaseValue = 1.0, }; - + dataLookup.Add(flotSeriesPolarAngle); } @@ -345,21 +346,18 @@ private List GetD3FrequencyDataLookup(VICycleDataGroup vICycleDataGrou #endregion #region [ Digitals Data ] - [Route("GetBreakerData"),HttpGet] + [Route("GetBreakerData"), HttpGet] public async Task GetBreakerData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); + ; + int eventId = int.Parse(Request.Query["eventId"].ToString()); Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection(connection.Connection, typeof(SqlDataAdapter), false); - - DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, meter); + DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, connection); List resultList = GetBreakerLookup(dataGroup); - + JsonReturn returnDict = new JsonReturn(); returnDict.Data = resultList; returnDict.EventStartTime = evt.StartTime.Subtract(m_epoch).TotalMilliseconds; @@ -372,16 +370,16 @@ public async Task GetBreakerData() private List GetBreakerLookup(DataGroup dataGroup) { - List dataLookup = dataGroup.DataSeries.Where(ds => ds.SeriesInfo.Channel.MeasurementType.Name == "Digital").Select((ds, i) => + List dataLookup = dataGroup.DataSeries.Where(ds => ds.SeriesInfo.Channel.MeasurementType.Name == "Digital").Select((ds, i) => new D3Series() - { - ChartLabel = (ds.SeriesInfo.Channel.Description == null) ? GetChartLabel(ds.SeriesInfo.Channel) : ds.SeriesInfo.Channel.Description, - Unit = "", - Color = $"Generic{i % 8 + 1}", - LegendHorizontal = "Digital", - LegendVertical = (ds.SeriesInfo.Channel.Description == null) ? GetChartLabel(ds.SeriesInfo.Channel) : ds.SeriesInfo.Channel.Description, - LegendGroup = ds.SeriesInfo.Channel.Asset.AssetName, - DataPoints = ds.DataPoints.Select(dataPoint => new double[] { dataPoint.Time.Subtract(m_epoch).TotalMilliseconds, dataPoint.Value }).ToList(), + { + ChartLabel = (ds.SeriesInfo.Channel.Description == null) ? GetChartLabel(ds.SeriesInfo.Channel) : ds.SeriesInfo.Channel.Description, + Unit = "", + Color = $"Generic{i % 8 + 1}", + LegendHorizontal = "Digital", + LegendVertical = (ds.SeriesInfo.Channel.Description == null) ? GetChartLabel(ds.SeriesInfo.Channel) : ds.SeriesInfo.Channel.Description, + LegendGroup = ds.SeriesInfo.Channel.Asset.AssetName, + DataPoints = ds.DataPoints.Select(dataPoint => new double[] { dataPoint.Time.Subtract(m_epoch).TotalMilliseconds, dataPoint.Value }).ToList(), }).ToList(); AdjustLegendNumbering(dataLookup); @@ -407,18 +405,12 @@ private void AdjustLegendNumbering(List data) [Route("GetAnalogsData"), HttpGet] public async Task GetAnalogsData() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - - Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); - meter.ConnectionFactory = () => new AdoDataConnection(connection.Connection, typeof(SqlDataAdapter), false); - - DataGroup dataGroup = await QueryDataGroupAsync(evt.ID, meter); + int eventId = int.Parse(Request.Query["eventId"].ToString()); + DataGroup dataGroup = await QueryDataGroupAsync(eventId, connection); List returnList = GetAnalogsLookup(dataGroup); - + JsonReturn returnDict = new JsonReturn(); returnDict.Data = returnList; DownSample(returnDict); @@ -429,14 +421,14 @@ public async Task GetAnalogsData() private List GetAnalogsLookup(DataGroup dataGroup) { - List dataLookup = dataGroup.DataSeries.Where(ds => - ds.SeriesInfo.Channel.MeasurementType.Name != "Digital" && - ds.SeriesInfo.Channel.MeasurementType.Name != "Voltage" && - ds.SeriesInfo.Channel.MeasurementType.Name != "Current" && - ds.SeriesInfo.Channel.MeasurementType.Name != "TripCoilCurrent").Select(ds => + List dataLookup = dataGroup.DataSeries.Where(ds => + ds.SeriesInfo.Channel.MeasurementType.Name != "Digital" && + ds.SeriesInfo.Channel.MeasurementType.Name != "Voltage" && + ds.SeriesInfo.Channel.MeasurementType.Name != "Current" && + ds.SeriesInfo.Channel.MeasurementType.Name != "TripCoilCurrent").Select(ds => new D3Series() { - ChartLabel = (ds.SeriesInfo.Channel.Description == null)? GetChartLabel(ds.SeriesInfo.Channel): ds.SeriesInfo.Channel.Description, + ChartLabel = (ds.SeriesInfo.Channel.Description == null) ? GetChartLabel(ds.SeriesInfo.Channel) : ds.SeriesInfo.Channel.Description, Unit = "", Color = GetColor(ds.SeriesInfo.Channel), LegendHorizontal = ds.SeriesInfo.Channel.Asset.AssetKey, @@ -449,19 +441,17 @@ private List GetAnalogsLookup(DataGroup dataGroup) } #endregion - + #region [ Info ] - [Route("GetHeaderData"),HttpGet] + [Route("GetHeaderData"), HttpGet] public Dictionary GetHeaderData() { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); - string breakerOperationID = (query.ContainsKey("breakeroperation") ? query["breakeroperation"] : "-1"); + int eventId = int.Parse(Request.Query["eventId"].ToString()); Dictionary returnDict = new Dictionary(); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { EventView theEvent = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); @@ -473,6 +463,7 @@ public Dictionary GetHeaderData() returnDict.Add("AssetName", theEvent.AssetName); returnDict.Add("EventName", theEvent.EventTypeName); returnDict.Add("EventDate", theEvent.StartTime.ToString("yyyy-MM-dd HH:mm:ss.fffffff")); + returnDict.Add("EventEnd", theEvent.EndTime.ToString("yyyy-MM-dd HH:mm:ss.fffffff")); returnDict.Add("Date", theEvent.StartTime.ToShortDateString()); returnDict.Add("EventMilliseconds", theEvent.StartTime.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds); returnDict.Add("xdaInstance", connection.ExecuteScalar("SELECT Value FROM DashSettings WHERE Name = 'System.XDAInstance'")); @@ -518,13 +509,13 @@ public Dictionary GetHeaderData() } else if (new List() { "Sag", "Swell" }.Contains(returnDict["EventName"])) { - + List disturbances = new TableOperations(connection) .QueryRecordsWhere("EventID = {0}", theEvent.ID) .Where(row => row.EventTypeID == theEvent.EventTypeID) .OrderBy(row => row.StartTime) - .ToList(); - + .ToList(); + openXDA.Model.Disturbance firstDisturbance = disturbances.FirstOrDefault(); openXDA.Model.Disturbance lastDisturbance = disturbances.LastOrDefault(); @@ -544,22 +535,17 @@ public Dictionary GetHeaderData() } } - if (breakerOperationID != "") + if (Request.Query.ContainsKey("breakeroperation") && int.TryParse(Request.Query["breakeroperation"].ToString(), out int breakerOperationID)) { - int id; + BreakerOperation breakerRow = new TableOperations(connection).QueryRecordWhere("ID = {0}", breakerOperationID); - if (int.TryParse(breakerOperationID, out id)) + if (breakerRow != null) { - BreakerOperation breakerRow = new TableOperations(connection).QueryRecordWhere("ID = {0}", id); - - if (breakerRow != null) - { - returnDict.Add("BreakerNumber", breakerRow.BreakerNumber); - returnDict.Add("BreakerPhase", new TableOperations(connection).QueryRecordWhere("ID = {0}", breakerRow.PhaseID).Name); - returnDict.Add("BreakerTiming", breakerRow.BreakerTiming.ToString()); - returnDict.Add("BreakerSpeed", breakerRow.BreakerSpeed.ToString()); - returnDict.Add("BreakerOperation", connection.ExecuteScalar("SELECT Name FROM BreakerOperationType WHERE ID = {0}", breakerRow.BreakerOperationTypeID).ToString()); - } + returnDict.Add("BreakerNumber", breakerRow.BreakerNumber); + returnDict.Add("BreakerPhase", new TableOperations(connection).QueryRecordWhere("ID = {0}", breakerRow.PhaseID).Name); + returnDict.Add("BreakerTiming", breakerRow.BreakerTiming.ToString()); + returnDict.Add("BreakerSpeed", breakerRow.BreakerSpeed.ToString()); + returnDict.Add("BreakerOperation", connection.ExecuteScalar("SELECT Name FROM BreakerOperationType WHERE ID = {0}", breakerRow.BreakerOperationTypeID).ToString()); } } @@ -567,13 +553,12 @@ public Dictionary GetHeaderData() } } - [Route("GetNavData"), HttpGet] - public Dictionary> GetNavData() - { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); + [Route("GetNavData"), HttpGet] + public Dictionary> GetNavData() + { + int eventId = int.Parse(Request.Query["eventId"].ToString()); - Dictionary> nextBackLookup = new Dictionary>() + Dictionary> nextBackLookup = new Dictionary>() { { "System", Tuple.Create((EventView)null, (EventView)null) }, { "Station", Tuple.Create((EventView)null, (EventView)null) }, @@ -581,79 +566,81 @@ public Dictionary> GetNavData() { "Asset", Tuple.Create((EventView)null, (EventView)null) } }; - Func func = inputString => { - switch (inputString) + Func func = inputString => { - case "System": - return "GetPreviousAndNextEventIdsForSystem"; - case "Station": - return "GetPreviousAndNextEventIdsForMeterLocation"; - case "Meter": - return "GetPreviousAndNextEventIdsForMeter"; - default: - return "GetPreviousAndNextEventIdsForLine"; - } + switch (inputString) + { + case "System": + return "GetPreviousAndNextEventIdsForSystem"; + case "Station": + return "GetPreviousAndNextEventIdsForMeterLocation"; + case "Meter": + return "GetPreviousAndNextEventIdsForMeter"; + default: + return "GetPreviousAndNextEventIdsForLine"; + } - }; + }; - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) - { - EventView theEvent = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - using (IDbCommand cmd = connection.Connection.CreateCommand()) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - cmd.CommandType = CommandType.StoredProcedure; - cmd.Parameters.Add(new SqlParameter("@EventID", eventId)); - cmd.CommandTimeout = 300; - - foreach (string procedure in nextBackLookup.Keys.ToList()) + EventView theEvent = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); + using (IDbCommand cmd = connection.Connection.CreateCommand()) { - EventView back = null; - EventView next = null; - int backID = -1; - int nextID = -1; - - cmd.CommandText = func(procedure); + cmd.CommandType = CommandType.StoredProcedure; + cmd.Parameters.Add(new SqlParameter("@EventID", eventId)); + cmd.CommandTimeout = 300; - using (IDataReader rdr = cmd.ExecuteReader()) + foreach (string procedure in nextBackLookup.Keys.ToList()) { - rdr.Read(); + EventView back = null; + EventView next = null; + int backID = -1; + int nextID = -1; - if (!rdr.IsDBNull(0)) - { - backID = rdr.GetInt32(0); - } + cmd.CommandText = func(procedure); - if (!rdr.IsDBNull(1)) + using (IDataReader rdr = cmd.ExecuteReader()) { - nextID = rdr.GetInt32(1); + rdr.Read(); + + if (!rdr.IsDBNull(0)) + { + backID = rdr.GetInt32(0); + } + + if (!rdr.IsDBNull(1)) + { + nextID = rdr.GetInt32(1); + } } - } - back = new TableOperations(connection).QueryRecordWhere("ID = {0}", backID); - next = new TableOperations(connection).QueryRecordWhere("ID = {0}", nextID); - nextBackLookup[procedure] = Tuple.Create(back, next); + back = new TableOperations(connection).QueryRecordWhere("ID = {0}", backID); + next = new TableOperations(connection).QueryRecordWhere("ID = {0}", nextID); + nextBackLookup[procedure] = Tuple.Create(back, next); + } } + return nextBackLookup; } - return nextBackLookup; } - } - - #endregion + + #endregion #region [ Compare ] - [Route("GetOverlappingEvents"),HttpGet] + [Route("GetOverlappingEvents"), HttpGet] public DataTable GetOverlappingEvents() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); + int eventId = int.Parse(Request.Query["eventId"].ToString()); Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventId); - DateTime startTime = ((query.ContainsKey("startDate") && query["startDate"] != "null") ? DateTime.Parse(query["startDate"]) : evt.StartTime); - DateTime endTime = ((query.ContainsKey("endDate") && query["endDate"] != "null") ? DateTime.Parse(query["endDate"]) : evt.EndTime); + DateTime startTime = (Request.Query.ContainsKey("startDate") && Request.Query["startDate"].ToString() != "null") ? + DateTime.Parse(Request.Query["startDate"].ToString()) : evt.StartTime; + DateTime endTime = (Request.Query.ContainsKey("endDate") && Request.Query["endDate"].ToString() != "null") ? + DateTime.Parse(Request.Query["endDate"].ToString()) : evt.EndTime; DataTable dataTable = connection.RetrieveData(@" @@ -707,13 +694,12 @@ FROM Disturbance #endregion #region [ UI Widgets ] - [Route("GetScalarStats"),HttpGet] + [Route("GetScalarStats"), HttpGet] public Dictionary GetScalarStats() { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); + int eventId = int.Parse(Request.Query["eventId"].ToString()); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { DataTable dataTable = connection.RetrieveData("SELECT * FROM OpenSEEScalarStatView WHERE EventID = {0}", eventId); if (dataTable.Rows.Count == 0) return new Dictionary(); @@ -724,13 +710,12 @@ public Dictionary GetScalarStats() } } - [Route("GetHarmonics"),HttpGet] + [Route("GetHarmonics"), HttpGet] public DataTable GetHarmonics() { - Dictionary query = Request.QueryParameters(); - int eventId = int.Parse(query["eventId"]); + int eventId = int.Parse(Request.Query["eventId"].ToString()); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { DataTable dataTable = connection.RetrieveData(@" SELECT @@ -751,11 +736,10 @@ SnapshotHarmonics JOIN [Route("GetTimeCorrelatedSags"), HttpGet] public DataTable GetTimeCorrelatedSags() { - Dictionary query = Request.QueryParameters(); - int eventID = int.Parse(query["eventId"]); + int eventID = int.Parse(Request.Query["eventId"].ToString()); if (eventID <= 0) return new DataTable(); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { double timeTolerance = connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'TimeTolerance'"); DateTime startTime = connection.ExecuteScalar("SELECT StartTime FROM Event WHERE ID = {0}", eventID); @@ -768,61 +752,76 @@ public DataTable GetTimeCorrelatedSags() } [Route("GetLightningData"), HttpGet] - public IEnumerable GetLightningData() + public IEnumerable GetLightningData(int eventID) { - Dictionary query = Request.QueryParameters(); - int eventID = int.Parse(query["eventID"]); - - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using AdoDataConnection connection = new(Settings.Default); + + const string QueryFormat = + "SELECT " + + " LightningStrike.Service, " + + " LightningStrike.UTCTime, " + + " LightningStrike.DisplayTime, " + + " LightningStrike.Amplitude, " + + " LightningStrike.Latitude, " + + " LightningStrike.Longitude, " + + " VaisalaExtendedLightningData.PeakCurrent, " + + " VaisalaExtendedLightningData.FlashMultiplicity, " + + " VaisalaExtendedLightningData.ParticipatingSensors, " + + " VaisalaExtendedLightningData.DegreesOfFreedom, " + + " VaisalaExtendedLightningData.EllipseAngle, " + + " VaisalaExtendedLightningData.SemiMajorAxisLength, " + + " VaisalaExtendedLightningData.SemiMinorAxisLength, " + + " VaisalaExtendedLightningData.ChiSquared, " + + " VaisalaExtendedLightningData.Risetime, " + + " VaisalaExtendedLightningData.PeakToZeroTime, " + + " VaisalaExtendedLightningData.MaximumRateOfRise, " + + " VaisalaExtendedLightningData.CloudIndicator, " + + " VaisalaExtendedLightningData.AngleIndicator, " + + " VaisalaExtendedLightningData.SignalIndicator, " + + " VaisalaExtendedLightningData.TimingIndicator " + + "FROM " + + " LightningStrike LEFT OUTER JOIN " + + " VaisalaExtendedLightningData ON VaisalaExtendedLightningData.LightningStrikeID = LightningStrike.ID " + + "WHERE EventID = {0}"; + + object ToLightningStrike(DataRow row) => new { - const string QueryFormat = - "SELECT * " + - "FROM " + - " LightningStrike LEFT OUTER JOIN " + - " VaisalaExtendedLightningData ON VaisalaExtendedLightningData.LightningStrikeID = LightningStrike.ID " + - "WHERE EventID = {0}"; - - object ToLightningStrike(DataRow row) => new - { - Service = row.ConvertField("Service"), - UTCTime = row.ConvertField("UTCTime"), - DisplayTime = row.ConvertField("DisplayTime"), - Amplitude = row.ConvertField("Amplitude"), - Latitude = row.ConvertField("Latitude"), - Longitude = row.ConvertField("Longitude"), - PeakCurrent = row.ConvertField("PeakCurrent"), - FlashMultiplicity = row.ConvertField("FlashMultiplicity"), - ParticipatingSensors = row.ConvertField("ParticipatingSensors"), - DegreesOfFreedom = row.ConvertField("DegreesOfFreedom"), - EllipseAngle = row.ConvertField("EllipseAngle"), - SemiMajorAxisLength = row.ConvertField("SemiMajorAxisLength"), - SemiMinorAxisLength = row.ConvertField("SemiMinorAxisLength"), - ChiSquared = row.ConvertField("ChiSquared"), - Risetime = row.ConvertField("Risetime"), - PeakToZeroTime = row.ConvertField("PeakToZeroTime"), - MaximumRateOfRise = row.ConvertField("MaximumRateOfRise"), - CloudIndicator = row.ConvertField("CloudIndicator"), - AngleIndicator = row.ConvertField("AngleIndicator"), - SignalIndicator = row.ConvertField("SignalIndicator"), - TimingIndicator = row.ConvertField("TimingIndicator") - }; + Service = row.ConvertField("Service"), + UTCTime = row.ConvertField("UTCTime"), + DisplayTime = row.ConvertField("DisplayTime"), + Amplitude = row.ConvertField("Amplitude"), + Latitude = row.ConvertField("Latitude"), + Longitude = row.ConvertField("Longitude"), + PeakCurrent = row.ConvertField("PeakCurrent"), + FlashMultiplicity = row.ConvertField("FlashMultiplicity"), + ParticipatingSensors = row.ConvertField("ParticipatingSensors"), + DegreesOfFreedom = row.ConvertField("DegreesOfFreedom"), + EllipseAngle = row.ConvertField("EllipseAngle"), + SemiMajorAxisLength = row.ConvertField("SemiMajorAxisLength"), + SemiMinorAxisLength = row.ConvertField("SemiMinorAxisLength"), + ChiSquared = row.ConvertField("ChiSquared"), + Risetime = row.ConvertField("Risetime"), + PeakToZeroTime = row.ConvertField("PeakToZeroTime"), + MaximumRateOfRise = row.ConvertField("MaximumRateOfRise"), + CloudIndicator = row.ConvertField("CloudIndicator"), + AngleIndicator = row.ConvertField("AngleIndicator"), + SignalIndicator = row.ConvertField("SignalIndicator"), + TimingIndicator = row.ConvertField("TimingIndicator") + }; - return connection - .RetrieveData(QueryFormat, eventID) - .AsEnumerable() - .Select(ToLightningStrike); - } + return connection + .RetrieveData(QueryFormat, eventID) + .AsEnumerable() + .Select(ToLightningStrike); } [Route("GetOutputChannelCount/{eventID}"), HttpGet] - public IHttpActionResult GetOutputChannelCount(int eventID) + public ActionResult GetOutputChannelCount(int eventID) { - try + if (eventID <= 0) return BadRequest("Invalid EventID"); + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { - if (eventID <= 0) return BadRequest("Invalid EventID"); - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) - { - int count = connection.ExecuteScalar(@" + int count = connection.ExecuteScalar(@" SELECT COUNT(*) FROM @@ -833,12 +832,7 @@ Event JOIN WHERE Event.ID = {0} ", eventID); - return Ok(count); - } - } - catch(Exception ex) - { - return InternalServerError(ex); + return Ok(count); } } @@ -848,9 +842,9 @@ Event JOIN #region [ Note Management ] [Route("GetPQBrowser"), HttpGet] - public IHttpActionResult GetPQBrowser() + public ActionResult GetPQBrowser() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { string pqBrowserURl = connection.ExecuteScalar(@" SELECT diff --git a/src/OpenSEE/Controllers/OpenSeeControllerBase.cs b/src/OpenSEE/Controllers/OpenSeeControllerBase.cs index dc1e6cd5..4777d96c 100644 --- a/src/OpenSEE/Controllers/OpenSeeControllerBase.cs +++ b/src/OpenSEE/Controllers/OpenSeeControllerBase.cs @@ -26,16 +26,18 @@ using System.Linq; using System.Runtime.Caching; using System.Threading.Tasks; -using System.Web.Http; using FaultData.DataAnalysis; -using GSF.Data; -using GSF.NumericalAnalysis; +using Gemstone.Configuration; +using Gemstone.Data; +using Gemstone.Data.Model; +using Gemstone.Numeric.Interpolation; +using Microsoft.AspNetCore.Mvc; using OpenSEE.Model; using openXDA.Model; namespace OpenSEE { - public class OpenSEEBaseController : ApiController + public class OpenSEEBaseController : Controller { #region [ Members ] @@ -59,7 +61,7 @@ public static double Sbase { { if (m_Sbase != null) return (double)m_Sbase; - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) m_Sbase = connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'SystemMVABase'") ?? 100.0; return (double)m_Sbase; } @@ -71,7 +73,7 @@ public static double Fbase { if (m_Fbase != null) return (double)m_Fbase; - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) m_Fbase = connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'SystemFrequency'")?? 60.0; return (double)m_Fbase; } @@ -84,7 +86,7 @@ public static int MaxSampleRate if (m_MaxSampleRate != null) return (int)m_MaxSampleRate; - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) m_MaxSampleRate = int.Parse(connection.ExecuteScalar("SELECT Value FROM [OpenSee.Setting] WHERE Name = 'maxSampleRate'") ?? "-1"); return (int)m_MaxSampleRate; } @@ -97,7 +99,7 @@ public static int MinSampleRate if (m_MinSampleRate != null) return (int)m_MinSampleRate; - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) m_MinSampleRate = int.Parse(connection.ExecuteScalar("SELECT Value FROM [OPenSee.Setting] WHERE Name = 'minSampleRate'") ?? "-1"); return (int)m_MinSampleRate; } @@ -130,7 +132,7 @@ public static string GetColor(Channel channel) return "random"; } - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { if (channel.MeasurementType.Name == "Voltage") @@ -203,21 +205,20 @@ protected double GetBaseV(Channel channel,bool rms) /// A color designation protected string GetFaultDistanceColor(string algorithm) { - string random = string.Format("#{0:X6}", m_random.Next(0x1000001)); switch (algorithm) { case ("Simple"): - return "faultDistSimple"; + return "Simple"; case ("Reactance"): - return "faultDistReact"; + return "Reactance"; case ("Takagi"): - return "faultDistTakagi"; + return "Takagi"; case ("ModifiedTakagi"): - return "faultDistModTakagi"; + return "ModifiedTakagi"; case ("Novosel"): - return "faultDistNovosel"; + return "Novosel"; case ("DoubleEnded"): - return "faultDistDoubleEnd"; + return "DoubleEnded"; default: return "random"; } @@ -234,13 +235,13 @@ protected string GetFrequencyColor(string phase) switch (phase) { case ("Avg"): - return "freqAll"; + return "All"; case ("AN"): - return "freqVa"; + return "Va"; case ("BN"): - return "freqVb"; + return "Vb"; case ("CN"): - return "freqVc"; + return "Vc"; default: return "random"; @@ -408,7 +409,7 @@ public static void DownSample(JsonReturn dict) #region [ Shared Functions ] - public static async Task QueryDataGroupAsync(int eventID, Meter meter) + public static async Task QueryDataGroupAsync(int eventID, AdoDataConnection connection) { string target = $"DataGroup-{eventID}"; @@ -423,8 +424,14 @@ public static async Task QueryDataGroupAsync(int eventID, Meter meter try { - List data = ChannelData.DataFromEvent(eventID, () => new AdoDataConnection("systemSettings")); - DataGroup dataGroup = ToDataGroup(meter, data); + Event evt = new TableOperations(connection).QueryRecordWhere("ID = {0}", eventID); + Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.MeterID); + meter.ConnectionFactory = () => new AdoDataConnection(Settings.Default); + Asset asset = new TableOperations(connection).QueryRecordWhere("ID = {0}", evt.AssetID); + asset.ConnectionFactory = () => new AdoDataConnection(Settings.Default); + + List data = ChannelData.DataFromEvent(eventID, () => new AdoDataConnection(Settings.Default)); + DataGroup dataGroup = ToDataGroup(meter, asset, data); taskCompletionSource.SetResult(dataGroup); return dataGroup; } @@ -436,7 +443,7 @@ public static async Task QueryDataGroupAsync(int eventID, Meter meter } } - public static async Task QueryVIDataGroupAsync(int eventID, Meter meter) + public static async Task QueryVIDataGroupAsync(int eventID, AdoDataConnection connection) { string target = $"VIDataGroup-{eventID}"; @@ -451,7 +458,7 @@ public static async Task QueryVIDataGroupAsync(int eventID, Meter m try { - DataGroup dataGroup = await QueryDataGroupAsync(eventID, meter); + DataGroup dataGroup = await QueryDataGroupAsync(eventID, connection); VIDataGroup viDataGroup = new VIDataGroup(dataGroup); taskCompletionSource.SetResult(viDataGroup); return viDataGroup; @@ -464,7 +471,7 @@ public static async Task QueryVIDataGroupAsync(int eventID, Meter m } } - public static async Task QueryVICycleDataGroupAsync(int eventID, Meter meter, bool compress = true) + public static async Task QueryVICycleDataGroupAsync(int eventID, AdoDataConnection connection, bool compress = true) { string compression = compress ? "compressed" : "uncompressed"; string target = $"VICycleDataGroup-{eventID}-{compression}"; @@ -480,7 +487,7 @@ public static async Task QueryVICycleDataGroupAsync(int eventI try { - VIDataGroup viDataGroup = await QueryVIDataGroupAsync(eventID, meter); + VIDataGroup viDataGroup = await QueryVIDataGroupAsync(eventID, connection); VICycleDataGroup viCycleDataGroup = Transform.ToVICycleDataGroup(viDataGroup, Fbase, compress); taskCompletionSource.SetResult(viCycleDataGroup); return viCycleDataGroup; @@ -493,9 +500,9 @@ public static async Task QueryVICycleDataGroupAsync(int eventI } } - public static DataGroup ToDataGroup(Meter meter, List data) + public static DataGroup ToDataGroup(Meter meter, Asset asset, List data) { - DataGroup dataGroup = new DataGroup(); + DataGroup dataGroup = new DataGroup(asset); dataGroup.FromData(meter, data); VIDataGroup vIDataGroup = new VIDataGroup(dataGroup); return vIDataGroup.ToDataGroup(); @@ -517,4 +524,4 @@ protected IDbDataParameter ToDateTime2(AdoDataConnection connection, DateTime da } -} \ No newline at end of file +} diff --git a/src/OpenSEE/FaultSpecifics.aspx b/src/OpenSEE/FaultSpecifics.aspx deleted file mode 100644 index 2398118d..00000000 --- a/src/OpenSEE/FaultSpecifics.aspx +++ /dev/null @@ -1,68 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="true" CodeFile="FaultSpecifics.aspx.cs" Inherits="FaultSpecifics" %> - - - - - - Fault Specifics - - - - - - - - - - -
- - - - - - - - - - - - - -
<%=postedMeterName %>
Fault Type:    <%=postedFaultType %>
Start Time:    <%=postedStartTime %>
Inception Time:    <%=postedInceptionTime %>
Delta Time:    <%=postedDeltaTime %>
Fault Duration:    <%=postedDurationPeriod %>
Fault Current:    <%=postedFaultCurrent %>
Distance Method:    <%=postedDistanceMethod %>
Single-ended Distance:    <%=postedSingleEndedDistance %>
Double-ended Distance:    <%=postedDoubleEndedDistance %>
Double-ended Angle:    <%=postedDoubleEndedConfidence %>
OpenXDA EventID:    <%=postedEventId %>
-
- - \ No newline at end of file diff --git a/src/OpenSEE/FaultSpecifics.aspx.cs b/src/OpenSEE/FaultSpecifics.aspx.cs deleted file mode 100644 index 9e44f373..00000000 --- a/src/OpenSEE/FaultSpecifics.aspx.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Data; -using System.Data.SqlClient; -using System.Globalization; -using System.Linq; -using GSF.Configuration; -using GSF.Data; -using GSF.Data.Model; -using System.Web.UI; -using openXDA.Model; -public partial class FaultSpecifics : Page -{ - public string postedFaultType = ""; - public string postedDeltaTime = ""; - public string postedStartTime = ""; - public string postedInceptionTime = ""; - public string postedDurationPeriod = ""; - public string postedFaultCurrent = ""; - public string postedDistanceMethod = ""; - public string postedSingleEndedDistance = ""; - public string postedEventId = ""; - public string postedMeterId = ""; - public string postedMeterName = ""; - public string postedDoubleEndedDistance = ""; - public string postedDoubleEndedConfidence = ""; - public string postedExceptionMessage = ""; - - string connectionstring = ConfigurationFile.Current.Settings["systemSettings"]["ConnectionString"].Value; - - protected void Page_Load(object sender, EventArgs e) - { - SqlConnection conn = null; - SqlDataReader rdr = null; - - if (!IsPostBack) - { - if (Request["eventId"] != null) - { - postedEventId = Request["eventId"]; - - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) - { - try - { - Event theevent = (new TableOperations(connection)).QueryRecordWhere("ID = {0}", Convert.ToInt32(postedEventId)); - FaultSummary thesummary = (new TableOperations(connection)).QueryRecordWhere("EventID = {0} AND IsSelectedAlgorithm = 1", Convert.ToInt32(postedEventId)); - - if ((object)thesummary == null) - { - postedFaultType = "Invalid"; - postedInceptionTime = "Invalid"; - postedDurationPeriod = "Invalid"; - postedFaultCurrent = "Invalid"; - postedDistanceMethod = "Invalid"; - postedSingleEndedDistance = "Invalid"; - postedDeltaTime = "Invalid"; - postedDoubleEndedDistance = "Invalid"; - postedDoubleEndedConfidence = "Invalid"; - return; - } - - postedFaultType = thesummary.FaultType; - postedInceptionTime = thesummary.Inception.TimeOfDay.ToString(); - postedDurationPeriod = (thesummary.DurationSeconds * 1000).ToString("##.###", CultureInfo.InvariantCulture) + "msec (" + thesummary.DurationCycles.ToString("##.##", CultureInfo.InvariantCulture) + " cycles)"; - postedFaultCurrent = thesummary.CurrentMagnitude.ToString("####.#", CultureInfo.InvariantCulture) + " Amps (RMS)"; - postedDistanceMethod = thesummary.Algorithm; - postedSingleEndedDistance = thesummary.Distance.ToString("####.###", CultureInfo.InvariantCulture) + " miles"; - double deltatime = (thesummary.Inception - theevent.StartTime).Ticks / 10000000.0; - postedDeltaTime = deltatime.ToString(); - postedStartTime = theevent.StartTime.TimeOfDay.ToString(); - postedMeterName = connection.ExecuteScalar("SELECT Name From Meter WHERE ID = {0}", theevent.MeterID); - postedMeterId = theevent.MeterID.ToString(); - - conn = new SqlConnection(connectionstring); - conn.Open(); - SqlCommand cmd = new SqlCommand("dbo.selectDoubleEndedFaultDistanceForEventID", conn); - cmd.CommandType = CommandType.StoredProcedure; - cmd.Parameters.Add(new SqlParameter("@EventID", postedEventId)); - cmd.CommandTimeout = 300; - rdr = cmd.ExecuteReader(); - - if (rdr.HasRows) - { - while (rdr.Read()) - { - postedDoubleEndedDistance = ((double)rdr["Distance"]).ToString("####.###", CultureInfo.InvariantCulture) + " miles"; - postedDoubleEndedConfidence = ((double)rdr["Angle"]).ToString("####.####", CultureInfo.InvariantCulture) + " degrees"; - } - } - } - catch (Exception ex) - { - postedExceptionMessage = ex.Message; - } - finally - { - if (rdr != null) - rdr.Dispose(); - - if (conn != null) - conn.Dispose(); - } - } - } - } - } -} \ No newline at end of file diff --git a/src/OpenSEE/Global.asax b/src/OpenSEE/Global.asax deleted file mode 100644 index cc344007..00000000 --- a/src/OpenSEE/Global.asax +++ /dev/null @@ -1 +0,0 @@ -<%@ Application Codebehind="Global.asax.cs" Inherits="OpenSEE.MvcApplication" Language="C#" %> diff --git a/src/OpenSEE/Global.asax.cs b/src/OpenSEE/Global.asax.cs deleted file mode 100644 index aae1a995..00000000 --- a/src/OpenSEE/Global.asax.cs +++ /dev/null @@ -1,186 +0,0 @@ -//****************************************************************************************************** -// Global.asax.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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 02/19/2020 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - -using System; -using System.Collections.Generic; -using System.Data; -using System.IO; -using System.Linq; -using System.Threading; -using System.Web; -using System.Web.Mvc; -using System.Web.Optimization; -using System.Web.Routing; -using GSF; -using GSF.Configuration; -using GSF.Data; -using GSF.Identity; -using GSF.IO; -using GSF.Security; -using GSF.Web.Embedded; -using GSF.Web.Model; -using OpenSEE.Model; - -namespace OpenSEE -{ - public class MvcApplication : HttpApplication - { - /// - /// Gets the default model used for the application. - /// - public static readonly AppModel DefaultModel = new AppModel(); - - protected void Application_Start() - { - Directory.SetCurrentDirectory(FilePath.GetAbsolutePath("")); - - AreaRegistration.RegisterAllAreas(); - FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); - RouteConfig.RegisterRoutes(RouteTable.Routes); - BundleConfig.RegisterBundles(BundleTable.Bundles); - - // Add additional virtual path provider to allow access to embedded resources - EmbeddedResourceProvider.Register(); - - GlobalSettings global = DefaultModel.Global; - - // Make sure LSCVSReport specific default config file service settings exist - CategorizedSettingsElementCollection systemSettings = ConfigurationFile.Current.Settings["systemSettings"]; - CategorizedSettingsElementCollection securityProvider = ConfigurationFile.Current.Settings["securityProvider"]; - - systemSettings.Add("ConnectionString", "Data Source=localhost; Initial Catalog=OpenSee; Integrated Security=SSPI", "Configuration connection string."); - systemSettings.Add("DataProviderString", "AssemblyName={System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089}; ConnectionType=System.Data.SqlClient.SqlConnection; AdapterType=System.Data.SqlClient.SqlDataAdapter", "Configuration database ADO.NET data provider assembly type creation string used"); - systemSettings.Add("CompanyName", "Grid Protection Alliance", "The name of the company who owns this instance of the openSee."); - systemSettings.Add("CompanyAcronym", "GPA", "The acronym representing the company who owns this instance of the openSee."); - systemSettings.Add("DateFormat", "MM/dd/yyyy", "The default date format to use when rendering timestamps."); - systemSettings.Add("TimeFormat", "HH:mm.ss.fff", "The default time format to use when rendering timestamps."); - systemSettings.Add("DefaultSecurityRoles", "Administrator, Manager, Engineer", "The default security roles that should exist for the application."); - securityProvider.Add("PasswordRequirementsRegex", AdoSecurityProvider.DefaultPasswordRequirementsRegex, "Regular expression used to validate new passwords for database users."); - securityProvider.Add("PasswordRequirementsError", AdoSecurityProvider.DefaultPasswordRequirementsError, "Error message to be displayed when new database user password fails regular expression test."); - - // Load default configuration file based model settings - global.CompanyName = systemSettings["CompanyName"].Value; - global.CompanyAcronym = systemSettings["CompanyAcronym"].Value; - global.DateFormat = systemSettings["DateFormat"].Value; - global.TimeFormat = systemSettings["TimeFormat"].Value; - global.DateTimeFormat = $"{global.DateFormat} {global.TimeFormat}"; - global.PasswordRequirementsRegex = securityProvider["PasswordRequirementsRegex"].Value; - global.PasswordRequirementsError = securityProvider["PasswordRequirementsError"].Value; - - // Load database driven model settings - using (DataContext dataContext = new DataContext(exceptionHandler: LogException)) - { - - // Load global web settings - Dictionary appSetting = dataContext.LoadDatabaseSettings("app.setting"); - global.ApplicationName = appSetting.TryGetValue("applicationName", out string setting)? setting : "OpenSEE"; - global.ApplicationDescription = appSetting.TryGetValue("applicationDescription", out setting) ? setting : "Event Viewing Engine"; - global.ApplicationKeywords = appSetting.TryGetValue("applicationKeywords", out setting) ? setting : "open source, utility, browser, power quality, management"; - global.BootstrapTheme = appSetting.TryGetValue("bootstrapTheme", out setting) ? setting : "~/Content/bootstrap-theme.css"; - - // Cache application settings - foreach (KeyValuePair item in appSetting) - global.ApplicationSettings.Add(item.Key, item.Value); - } - } - - private void Page_Error(object sender, EventArgs e) - { - Exception exc = Server.GetLastError(); - WriteToErrorLog(exc); - - // Clear the error from the server. - Server.ClearError(); - } - - void Application_Error(object sender, EventArgs e) - { - Exception exc = Server.GetLastError(); - WriteToErrorLog(exc); - } - - /// - /// Logs a status message. - /// - /// Message to log. - /// Type of message to log. - public static void LogStatusMessage(string message, UpdateType type = UpdateType.Information) - { - // TODO: Write message to log with log4net, etc. - } - - /// - /// Logs an exception. - /// - /// Exception to log. - public static void LogException(Exception ex) - { - // TODO: Write exception to log with log4net, etc. -#if DEBUG - ThreadPool.QueueUserWorkItem(state => - { - Thread.Sleep(1500); - }); -#endif - WriteToErrorLog(ex); - } - - private static ReaderWriterLockSlim LogFileReadWriteLock = new ReaderWriterLockSlim(); - - public static void WriteToErrorLog(Exception ex, bool innerException = false) - { - if (ex.InnerException != null) WriteToErrorLog(ex.InnerException, true); - - string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "OpenSee"); - string path = Path.Combine("C:\\Users\\Public\\Documents", "OpenSee.ErrorLog.txt"); - // Set Status to Locked - LogFileReadWriteLock.EnterWriteLock(); - try - { - Directory.CreateDirectory(folderPath); - // Append text to the file - using (StreamWriter sw = File.AppendText(path)) - { - sw.WriteLine($"[{DateTime.Now}] ({(innerException ? "Inner Exception" : "Outer Excpetion")})"); - sw.WriteLine($"Exception Source: {ex.Source}"); - sw.WriteLine($"Exception Message: {ex.Message}"); - sw.WriteLine(); - sw.WriteLine("---- Stack Trace ----"); - sw.WriteLine(); - sw.WriteLine(ex.StackTrace); - sw.WriteLine(); - sw.WriteLine(); - - sw.Close(); - } - } - finally - { - // Release lock - LogFileReadWriteLock.ExitWriteLock(); - } - } - - - } -} diff --git a/src/OpenSEE/Images/openSEE.jpg b/src/OpenSEE/Images/openSEE.jpg deleted file mode 100644 index 4931da35..00000000 Binary files a/src/OpenSEE/Images/openSEE.jpg and /dev/null differ diff --git a/src/OpenSEE/Images/openSEELogo.png b/src/OpenSEE/Images/openSEELogo.png deleted file mode 100644 index 3d9dcb1c..00000000 Binary files a/src/OpenSEE/Images/openSEELogo.png and /dev/null differ diff --git a/src/OpenSEE/Model/AppModel.cs b/src/OpenSEE/Model/AppModel.cs deleted file mode 100644 index 73f2fb6d..00000000 --- a/src/OpenSEE/Model/AppModel.cs +++ /dev/null @@ -1,81 +0,0 @@ -//****************************************************************************************************** -// AppModel.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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 02/19/2020 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - - -using GSF; -using GSF.Data.Model; -using GSF.Web; -using GSF.Web.Model; -using System; -using System.Web; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Path = System.Web.VirtualPathUtility; -using System.Web.Routing; - -namespace OpenSEE.Model -{ - /// - /// Defines a base application model with convenient global settings and functions. - /// - /// - /// Custom view models should inherit from AppModel because the "Global" property is used by _Layout.cshtml. - /// - public class AppModel - { - #region [ Constructors ] - - /// - /// Creates a new . - /// - public AppModel() - { - Global = MvcApplication.DefaultModel != null ? MvcApplication.DefaultModel.Global : new GlobalSettings(); - } - - #endregion - - #region [ Properties ] - - /// - /// Gets global settings for application. - /// - public GlobalSettings Global { get; } - #endregion - - #region [ Methods ] - - public bool IsDebug() - { -#if DEBUG - return true; -#else - return false; -#endif - - } - #endregion - } -} \ No newline at end of file diff --git a/src/OpenSEE/Model/D3Series.cs b/src/OpenSEE/Model/D3Series.cs index 2d5b2db0..39e651e1 100644 --- a/src/OpenSEE/Model/D3Series.cs +++ b/src/OpenSEE/Model/D3Series.cs @@ -21,18 +21,7 @@ // //****************************************************************************************************** - -using GSF; -using GSF.Data.Model; -using GSF.Web; -using GSF.Web.Model; -using System; -using System.Web; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - namespace OpenSEE.Model { diff --git a/src/OpenSEE/Model/FaultSpecifics.cs b/src/OpenSEE/Model/FaultSpecifics.cs new file mode 100644 index 00000000..86817876 --- /dev/null +++ b/src/OpenSEE/Model/FaultSpecifics.cs @@ -0,0 +1,52 @@ +//****************************************************************************************************** +// SystemSettings.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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 01/23/2026 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Data; +using Gemstone.Data; +using Gemstone.Data.Model; + +namespace OpenSEE.Models +{ + [TableName("openSee.FaultSpecifics"), UseEscapedName] + public class FaultSpecifics + { + [PrimaryKey(true)] + public int ID { get; set; } + public string FaultType { get; set; } + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime Inception { get; set; } + public double DurationMs { get; set; } + public double DurationCycles { get; set; } + public double DeltaTime { get; set; } + public double CurrentMagnitude { get; set; } + public string Algorithm { get; set; } + public double Distance { get; set; } + public double DoubleFaultDistance { get; set; } + public double DoubleFaultAngle { get; set; } + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime StartTime { get; set; } + public string MeterName { get; set; } + + } +} diff --git a/src/OpenSEE/Model/SystemSettings.cs b/src/OpenSEE/Model/SystemSettings.cs new file mode 100644 index 00000000..daa92b8a --- /dev/null +++ b/src/OpenSEE/Model/SystemSettings.cs @@ -0,0 +1,41 @@ +//****************************************************************************************************** +// SystemSettings.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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 01/23/2026 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace OpenSEE.Models +{ + public class SystemSettings + { + public string ConnectionString { get; set; } + public string DataProviderString { get; set; } + public string CompanyName { get; set; } + public string CompanyAcronym { get; set; } + public string DateFormat { get; set; } + public string TimeFormat { get; set; } + public string PasswordRequirementsRegex { get; set; } + public string PasswordRequirementsError { get; set; } + public string ApplicationName { get; set; } + public string ApplicationDescription { get; set; } + public string ApplicationKeywords { get; set; } + public string BootstrapTheme { get; set; } + } +} diff --git a/src/OpenSEE/OpenSEE.csproj b/src/OpenSEE/OpenSEE.csproj index a7f4cdcd..d4491d96 100644 --- a/src/OpenSEE/OpenSEE.csproj +++ b/src/OpenSEE/OpenSEE.csproj @@ -1,337 +1,103 @@ - - - - + - Debug - AnyCPU - - - 2.0 - {845F68F7-4094-4FE6-95E3-1B113BBFAD3F} - {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties - OpenSEE - OpenSEE - v4.8 - false - true - - - - enabled - enabled - - - - + Exe Latest true - latest - - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - - - true - pdbonly - true + net9.0 bin\ - TRACE - prompt - 4 + false + true - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - CSVDownload.ashx - - - FaultSpecifics.aspx - ASPXCodeBehind - - - Global.asax - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - ..\Dependencies\openXDA\FaultAlgorithms.dll - - - ..\Dependencies\openXDA\FaultData.dll - - - False - ..\Dependencies\GSF\GSF.Core.dll - - - False - ..\Dependencies\GSF\GSF.PQDIF.dll - - - False - ..\Dependencies\GSF\GSF.Security.dll - - - False - ..\Dependencies\GSF\GSF.Web.dll - - - ..\Dependencies\GSF\Ionic.Zlib.dll - ..\Dependencies\NuGet\MathNet.Numerics.5.0.0-alpha02\lib\net48\MathNet.Numerics.dll - - ..\Dependencies\NuGet\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.4.1.0\lib\net472\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll - - - - ..\Dependencies\GSF\Microsoft.Owin.dll - - - ..\Dependencies\NuGet\Microsoft.Owin.Host.SystemWeb.3.1.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - - - ..\Dependencies\GSF\Microsoft.Owin.Security.dll - - - False - ..\Dependencies\GSF\Newtonsoft.Json.dll - - - ..\Dependencies\openXDA\openXDA.Model.dll - - - ..\Dependencies\GSF\Owin.dll - - - ..\Dependencies\openXDA\PQDS.dll - - - False - ..\Dependencies\GSF\RazorEngine.dll - - - - - - - - - - - - False - ..\Dependencies\GSF\System.Net.Http.Formatting.dll - - - - - - - - - - - - ..\Dependencies\GSF\System.Web.Http.dll - - - ..\Dependencies\GSF\System.Web.Http.Owin.dll - - - ..\Dependencies\GSF\System.Web.Mvc.dll - - - ..\Dependencies\NuGet\Microsoft.AspNet.Web.Optimization.1.1.3\lib\net40\System.Web.Optimization.dll - - - - - ..\Dependencies\NuGet\WebGrease.1.5.2\lib\WebGrease.dll - - - - - - - - + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - - - - - - - - - True - True - 58744 - / - http://localhost:44367/openSEE - False - False - - - False - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - CALL cd "$(ProjectDir)" -if $(ConfigurationName) == Debug npm run build -if $(ConfigurationName) == Release npm run buildrelease - - - - \ No newline at end of file + diff --git a/src/OpenSEE/Program.cs b/src/OpenSEE/Program.cs new file mode 100644 index 00000000..a8678ed5 --- /dev/null +++ b/src/OpenSEE/Program.cs @@ -0,0 +1,137 @@ +//****************************************************************************************************** +// Program.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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 01/23/2026 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.IO; +using Gemstone.Configuration; +using Gemstone.Data; +using Gemstone.Diagnostics; +using Gemstone.Threading; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Debug; +using OpenSEE.Models; + +namespace OpenSEE +{ + public class Program + { + public static IConfiguration Configuration { get; } = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true) + .Build(); + + public static void Main(string[] args) + { + try + { + ShutdownHandler.Initialize(); + + Settings settings = new() + { + INIFile = ConfigurationOperation.ReadWrite, + SQLite = ConfigurationOperation.Disabled + }; + + DefineSettings(settings); + + // Bind settings to configuration sources + settings.Bind(new ConfigurationBuilder() + .ConfigureGemstoneDefaults(settings) + .AddCommandLine(args, settings.SwitchMappings)); + + HostApplicationBuilderSettings appSettings = new() + { + Args = args, + ApplicationName = nameof(OpenSEE), + DisableDefaults = true, + }; + + CreateHostBuilder(args).Build().Run(); + + #if DEBUG + Settings.Save(forceSave: true); + #else + Settings.Save(); + #endif + } + finally + { + ShutdownHandler.InitiateSafeShutdown(); + } + } + + /// + /// Establishes default settings for the config file. + /// + public static void DefineSettings(Settings settings) + { + using (Logger.SuppressFirstChanceExceptionLogMessages()) + { + DiagnosticsLogger.DefineSettings(settings); + AdoDataConnection.DefineSettings(settings); + } + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .ConfigureServices((hostContext, services) => + { + // load settings from config file + services.Configure(hostContext.Configuration.GetSection("systemSettings")); + }) + .ConfigureLogging(builder => + { + builder.ClearProviders(); + builder.SetMinimumLevel(LogLevel.Information); + + builder.AddFilter("Microsoft", LogLevel.Warning); + builder.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Error); + builder.AddFilter("", LogLevel.Debug); + builder.AddFilter("", LogLevel.Trace); + + builder.AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Error); + builder.AddDebug(); + + // Add Gemstone diagnostics logging + builder.AddGemstoneDiagnostics(); + + #if RELEASE + if (OperatingSystem.IsWindows()) + { + builder.AddFilter("Application", LogLevel.Warning); + builder.AddEventLog(); + } + #endif + }); + } +} diff --git a/src/OpenSEE/Properties/launchSettings.json b/src/OpenSEE/Properties/launchSettings.json new file mode 100644 index 00000000..8e259806 --- /dev/null +++ b/src/OpenSEE/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "https://localhost:50951", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "OpenSEE": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:50951" + } + } +} \ No newline at end of file diff --git a/src/OpenSEE/Scripts/TSX/Components/About.tsx b/src/OpenSEE/Scripts/TSX/Components/About.tsx deleted file mode 100644 index cea0c40a..00000000 --- a/src/OpenSEE/Scripts/TSX/Components/About.tsx +++ /dev/null @@ -1,77 +0,0 @@ -//****************************************************************************************************** -// About.tsx - Gbtc -// -// Copyright © 2019, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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/29/2019 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - -import * as React from 'react'; -import { Modal } from '@gpa-gemstone/react-interactive' - -interface Iprops { - closeCallback: () => void, - isOpen: boolean, -} - -const About = (props: Iprops) => { - return ( - <> - { - props.closeCallback() - }} - ShowX={true} - ShowCancel={true} - CancelBtnClass={"btn btn-danger"} - ShowConfirm={false} - > -

Version 3.0

- -

openSEE is a browser-based waveform display and analytics tool that is used to view waveforms recorded by DFRs, Power Quality meters, relays and other substation devices that are stored in the openXDA database. - The link in the URL window of openSEE can be embedded in emails so that recipients can quickly access the waveforms being studied.

- -

General Navigation Features

- -

The navigational context of openSEE is relative to the "waveform-of-focus" -- the waveform displayed in the top-most collection of charts that is displayed when openSEE is first opened -- - typically after clicking a link to drill down into a specific waveform in the Open PQ Dashboard. - Tools in openSEE allow the user to dig deeper and understand more about this waveform-of-focus. - Tools in openSEE also enable users to easily change the waveform-of-focus from the initially loaded -- moving forward or back sequentially in time. -

- -
    -
  • Region Select Zooming - The waveform initially loads with the the time-scale set to the full length of the waveform capture. With the mouse, the user can select a region of the waveform to zoom in and see more detail.
  • -
  • Forward and Back Navigation - Using the collection of controls in the upper-right of the openSEE display, the user can select the basis for changing to a new waveform-of-focus. A selection of "system" means that user can step forward or back - to next event in the openXDA base globally (for all DFRs, PQ Meters, etc.), - i.e., what happened immediately previously or next on the system relative to the current waveform-of-focus. A selection of "asset" (or "line") limits this navigation to just events on this asset. - A selection of "meter" limits this navigation to just events recorded by this substation device.
  • -
  • Chart Trace Section - To the right of each chart, the user has the ability to turn on and off individual traces. Tabs are provided to organize these selections by data type.
  • -
- -

- The open-source code for openSEE can be found on GitHub. See: https://github.com/GridProtectionAlliance/openSEE -

- -
- - - - ); - -} - -export default About; \ No newline at end of file diff --git a/src/OpenSEE/Scripts/TSX/Components/AnalyticOptions.tsx b/src/OpenSEE/Scripts/TSX/Components/AnalyticOptions.tsx deleted file mode 100644 index 83912457..00000000 --- a/src/OpenSEE/Scripts/TSX/Components/AnalyticOptions.tsx +++ /dev/null @@ -1,280 +0,0 @@ -//****************************************************************************************************** -// RadioselectWindow.tsx - Gbtc -// -// Copyright © 2019, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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/13/2019 - Billy Ernest -// Generated original version of source code. -// 09/25/2019 - Christoph Lackner -// Added Settings Form -// -//****************************************************************************************************** - -import * as React from 'react'; -import { OpenSee } from '../global'; -import { SelectHarmonic,SelectHPF, SelectLPF, SelectTRC, SelectCycles, UpdateAnalytic, SelectAnalytics } from '../store/analyticSlice'; -import { SelectPlotKeys, RemovePlot, AddPlot, SelectEventIDs } from '../store/dataSlice' -import { useAppDispatch, useAppSelector } from '../hooks'; -import { BtnDropdown } from "@gpa-gemstone/react-interactive" -import { Select, Input } from "@gpa-gemstone/react-forms" -import * as _ from 'lodash' -import { ToInt } from '../store/queryThunk' -import { GetDisplayLabel } from '../Graphs/Utilities' - -const AnalyticOptions = () => { - const dispatch = useAppDispatch(); - const harmonic = useAppSelector(SelectHarmonic) - const plotKeys = useAppSelector(SelectPlotKeys) - const hpf = useAppSelector(SelectHPF) - const lpf = useAppSelector(SelectLPF) - const trc = useAppSelector(SelectTRC) - const cycles = useAppSelector(SelectCycles); - const analytics = useAppSelector(SelectAnalytics); - const eventIDs = useAppSelector(SelectEventIDs) - - const [isHarmonicValid, setIsHarmonicValid] = React.useState(true) - const [isFFTCyclesValid, setIsFFTCyclesValid] = React.useState(true) - - - const defaultAnalyticBtns = [ - { Label: 'Fault Distance', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'FaultDistance', EventId: id } }))), DataType: 'FaultDistance' }, - { Label: 'FFT', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'FFT', EventId: id } }))), DataType: 'FFT' }, - { Label: 'First Derivative', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'FirstDerivative', EventId: id } }))), DataType: "FirstDerivative" }, - { Label: 'Fix Clipped Waveforms', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'ClippedWaveforms', EventId: id } }))), DataType: 'ClippedWaveforms' }, - { Label: 'Frequency', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'Frequency', EventId: id } }))), DataType: 'Frequency' }, - { Label: 'High Pass', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'HighPassFilter', EventId: id } }))), DataType: 'HighPassFilter' }, - { Label: 'Impedance', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'Impedance', EventId: id } }))), DataType: 'Impedance' }, - { Label: 'Low Pass', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'LowPassFilter', EventId: id } }))), DataType: 'LowPassFilter' }, - { Label: 'Missing Voltage', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'MissingVoltage', EventId: id } }))), DataType: 'MissingVoltage' }, - { Label: 'Overlapping Waveform', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'OverlappingWave', EventId: id } }))), DataType: 'OverlappingWave' }, - { Label: 'Power', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'Power', EventId: id } }))), DataType: 'Power' }, - { Label: 'Rapid Voltage Change', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'RapidVoltage', EventId: id } }))), DataType: 'RapidVoltage' }, - { Label: 'Rectifier Output', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'Rectifier', EventId: id } }))), DataType: 'Rectifier' }, - { Label: 'Remove Current', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'RemoveCurrent', EventId: id } }))), DataType: 'RemoveCurrent' }, - { Label: 'Specified Harmonic', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'Harmonic', EventId: id } }))), DataType: 'Harmonic' }, - { Label: 'Symmetrical Components', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'SymetricComp', EventId: id } }))), DataType: 'SymetricComp' }, - { Label: 'THD', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'THD', EventId: id } }))), DataType: 'THD' }, - { Label: 'Unbalance', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'Unbalance', EventId: id } }))), DataType: 'Unbalance' }, - { Label: 'i2t', Callback: () => eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: 'I2T', EventId: id } }))), DataType: 'I2T' } - ]; - - const [analyticBtns, setAnalyticBtns] = React.useState(defaultAnalyticBtns) - - const handleAnalyticChange = (analyticParam: number, analytic: 'Harmonic' | 'FFT') => { - if (analytic === "Harmonic") { - if (analyticParam) { - eventIDs.forEach(id => { - setTimeout(() => { - dispatch(UpdateAnalytic({ settings: { ...analytics, Harmonic: ToInt(harmonic) }, key: { DataType: "Harmonic", EventId: id } })); - }, 500); - }); - setIsHarmonicValid(true) - } - else - setIsHarmonicValid(false) - } else if (analytic === "FFT") { - if (analyticParam) { - eventIDs.forEach(id => { - setTimeout(() => { - dispatch(UpdateAnalytic({ settings: { ...analytics, FFTCycles: ToInt(analyticParam) }, key: { DataType: "FFT", EventId: id } })); - }, 500); - }); - setIsFFTCyclesValid(true) - } - else - setIsFFTCyclesValid(false) - } - - } - - const options = { - order: [ - { Label: '1', Value: '1' }, - { Label: '2', Value: '2' }, - { Label: '3', Value: '3' }, - ], - trc: [ - { Label: '100', Value: '100' }, - { Label: '200', Value: '200' }, - { Label: '500', Value: '500' }, - ], - cycles: [ - { Label: '1', Value: '1' }, - { Label: '2', Value: '2' }, - { Label: '3', Value: '3' }, - { Label: '4', Value: '4' }, - { Label: '5', Value: '5' }, - { Label: '6', Value: '6' }, - { Label: '7', Value: '7' }, - { Label: '8', Value: '8' }, - { Label: '9', Value: '9' }, - { Label: '10', Value: '10' }, - { Label: '11', Value: '11' }, - { Label: '12', Value: '12' }, - { Label: '13', Value: '13' }, - { Label: '14', Value: '14' }, - { Label: '15', Value: '15' }, - ] - } - - React.useEffect(() => { - const filteredAnalyticBtns = defaultAnalyticBtns.filter(btn => !plotKeys.map(key => key.DataType).includes(btn.DataType as OpenSee.graphType)); - - setAnalyticBtns(filteredAnalyticBtns) - }, [plotKeys]) - - - //nonanalytic plots or analytic plots that need parameters - const dynamicPlots = ["Harmonic", "HighPassFilter", "LowPassFilter", "Rectifier", "FFT", "Voltage", "Current", "Analogs", 'Digitals', 'TripCoil'] - - return ( - <> -
-
-
- eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: analyticBtns[0].DataType, EventId: id } })))} - Options={analyticBtns} - /> -
- {plotKeys.some(type => type.DataType === "Harmonic") && ( -
-
- Specified Harmonic -
-
- handleAnalyticChange(ToInt(harmonic.harmonic), 'Harmonic')} - Label={"Harmonic:"} - Valid={() => isHarmonicValid} - Feedback="Harmonic value can not be empty" - /> -
-
- -
-
-
-
- )} - {plotKeys.some(type => type.DataType === "HighPassFilter") && ( -
-
- High Pass Filter -
-
- eventIDs.forEach(id => dispatch(UpdateAnalytic({ settings: { ...analytics, LPFOrder: ToInt(lpf.lpf) }, key: { DataType: "LowPassFilter", EventId: id } }))) } - Label={"Order:"} - /> -
-
- -
-
-
-
- )} - {plotKeys.some(type => type.DataType === "Rectifier") && ( -
-
- Rectifier -
-
- handleAnalyticChange(ToInt(cycles.cycles), 'FFT')} - Label={"Length(Cycles):"} - Valid={() => isFFTCyclesValid} - Feedback="FFT Cycles value can not be empty" - /> -
-
- -
-
-
-
- )} - {_.uniqBy(plotKeys, "DataType").map(key => !dynamicPlots.includes(key.DataType) && ( -
-
- {GetDisplayLabel(key.DataType)} -
-
- -
-
-
-
- ))} -
-
- - ); -} - - -export default AnalyticOptions; \ No newline at end of file diff --git a/src/OpenSEE/Scripts/TSX/Components/Menu.tsx b/src/OpenSEE/Scripts/TSX/Components/Menu.tsx deleted file mode 100644 index 85d6c2e2..00000000 --- a/src/OpenSEE/Scripts/TSX/Components/Menu.tsx +++ /dev/null @@ -1,158 +0,0 @@ -//****************************************************************************************************** -// Menu.tsx - Gbtc -// -// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 05/14/2018 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - -import * as React from 'react'; - -declare const homePath: string - -export default class Menu extends React.Component{ - props: { - eventID: number, - pointsButtonText: string, - tooltipButtonText: string, - phasorButtonText: string, - statButtonText: string, - harmonicButtonText: string, - lightningDataButtonText: string, - correlatedSagsButtonText: string, - enableLightningData: boolean, - postedMeterName: string, - postedEventName: string, - startDate: string, - endDate: string, - callback: Function, - exportCallback: Function - } - - constructor(props) { - super(props); - } - - componentDidMount() { - ($("#menu") as any).accordion({ - active: false, - collapsible: true - }); - } - - render() { - - return ( - - ); - } - - showhidePoints() { - if (this.props.pointsButtonText == "Show Points") { - this.props.callback({ pointsButtonText: "Hide Points" }); - $('#accumulatedpoints').show(); - } else { - this.props.callback({ pointsButtonText: "Show Points" }); - $('#accumulatedpoints').hide(); - } - } - - showhideTooltip() { - if (this.props.tooltipButtonText == "Show Tooltip") { - this.props.callback({ tooltipButtonText: "Hide Tooltip" }); - $('#unifiedtooltip').show(); - $('.legendCheckbox').show(); - - } else { - this.props.callback({ tooltipButtonText: "Show Tooltip" }); - $('#unifiedtooltip').hide(); - $('.legendCheckbox').hide(); - } - } - - showhidePhasor() { - if (this.props.phasorButtonText == "Show Phasor") { - this.props.callback({ phasorButtonText: "Hide Phasor" }); - $('#phasor').show(); - } else { - this.props.callback({ phasorButtonText: "Show Phasor" }); - $('#phasor').hide(); - } - } - - showhideStats() { - if (this.props.statButtonText == "Show Stats") { - this.props.callback({ statButtonText: "Hide Stats" }); - $('#scalarstats').show(); - } else { - this.props.callback({ statButtonText: "Show Stats" }); - $('#scalarstats').hide(); - } - } - - showhideCorrelatedSags() { - if (this.props.correlatedSagsButtonText == "Show Correlated Sags") { - this.props.callback({ correlatedSagsButtonText: "Hide Correlated Sags" }); - $('#correlatedsags').show(); - } else { - this.props.callback({ correlatedSagsButtonText: "Show Correlated Sags" }); - $('#correlatedsags').hide(); - } - } - - showhideHarmonics() { - if (this.props.harmonicButtonText == "Show Harmonics") { - this.props.callback({ harmonicButtonText: "Hide Harmonics" }); - $('#harmonicstats').show(); - } else { - this.props.callback({ harmonicButtonText: "Show Harmonics" }); - $('#harmonicstats').hide(); - } - } - - showhideLightningData() { - if (this.props.lightningDataButtonText == "Show Lightning Data") { - this.props.callback({ lightningDataButtonText: "Hide Lightning Data" }); - $('#lightningquery').show(); - } else { - this.props.callback({ lightningDataButtonText: "Show Lightning Data" }); - $('#lightningquery').hide(); - } - } - - exportComtrade() { - window.open(homePath + `OpenSEEComtradeDownload.ashx?eventID=${this.props.eventID}` + - `${this.props.startDate != undefined ? `&startDate=${this.props.startDate}` : ``}` + - `${this.props.endDate != undefined ? `&endDate=${this.props.endDate}` : ``}` + - `&Meter=${this.props.postedMeterName}` + - `&EventType=${this.props.postedEventName}`); - } -} diff --git a/src/OpenSEE/Scripts/TSX/Components/OpenSEENavbar.tsx b/src/OpenSEE/Scripts/TSX/Components/OpenSEENavbar.tsx deleted file mode 100644 index 05464fcc..00000000 --- a/src/OpenSEE/Scripts/TSX/Components/OpenSEENavbar.tsx +++ /dev/null @@ -1,632 +0,0 @@ -//****************************************************************************************************** -// OpenSEENavbar.tsx - Gbtc -// -// Copyright © 2019, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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/14/2019 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - -import * as React from 'react'; -import { OpenSee } from '../global'; -import { clone } from 'lodash'; -import { ResetZoom, SelectEventIDs, SelectFFTLimits, SelectDisplayed, AddPlot, RemovePlot, SelectFFTEnabled, SelectAnalytics } from '../store/dataSlice'; -import { SelectEventInfo, SelectLookupInfo } from '../store/eventInfoSlice' -import { SelectCycles, SelectHarmonic, SelectHPF, SelectLPF, SelectTRC } from '../store/analyticSlice'; -import { SelectNavigation, SetNavigation, SetMouseMode, SetZoomMode, SelectMouseMode } from '../store/settingSlice' - -import { WaveformViews, PhasorClock, statsIcon, lightningData, exportBtn, Zoom, Pan, FFT, Reset, Square, ValueRect, TimeRect, Settings, Help, ShowPoints, CorrelatedSags } from '../Graphs/ChartIcons'; -import { Point } from '@gpa-gemstone/gpa-symbols' -import { ToolTip } from '@gpa-gemstone/react-interactive'; -import { useAppDispatch, useAppSelector } from '../hooks'; -import moment from "moment" - -import About from './About'; - - -declare var homePath: string; -declare var eventStartTime: string; -declare var eventEndTime: string; - -interface IProps { - ToggleDrawer: (drawer: OpenSee.OverlayDrawers, open: boolean) => void, - OpenDrawers: OpenSee.Drawers - Width: number -} - -type Hover = ('None' | 'Waveform' | 'Show Points' | 'Polar Chart' | 'Stat' | 'Sags' | 'Lightning' | 'Export' | 'Tooltip' | 'Clock' | 'Zoom Mode' | 'Pan' | 'FFTTable' | 'FFTMove' | 'Reset Zoom' | 'Settings' | 'NavLeft' | 'NavRight' | 'Help' | 'Meter' | 'Station' | 'Asset' | 'EType' | 'EInception' | 'Select') - -const OpenSeeNavBar = (props: IProps) => { - const dispatch = useAppDispatch(); - const mouseMode = useAppSelector(SelectMouseMode); - const eventInfo = useAppSelector(SelectEventInfo); - const lookupInfo = useAppSelector(SelectLookupInfo); - const showFFT = useAppSelector(SelectFFTEnabled); - - const analytics = useAppSelector(SelectAnalytics); - - const navigation = useAppSelector(SelectNavigation); - const showPlots = useAppSelector(SelectDisplayed); - const harmonic = useAppSelector(SelectHarmonic); - const trc = useAppSelector(SelectTRC); - const lpf = useAppSelector(SelectLPF); - const hpf = useAppSelector(SelectHPF); - const cycles = useAppSelector(SelectCycles); - const fftTime = useAppSelector(SelectFFTLimits); - - const [showAbout, setShowAbout] = React.useState(false); - const [hover, setHover] = React.useState('None') - - - React.useEffect(() => { - if (props.OpenDrawers.AccumulatedPoints) { - let oldMode = clone(mouseMode); - dispatch(SetMouseMode('select')) - return () => { - dispatch(SetMouseMode(oldMode)) - } - } - return () => { } - - }, [props.OpenDrawers.AccumulatedPoints]) - - function exportData(type) { - window.open(homePath + `CSVDownload.ashx?type=${type}&eventID=${eventID}` + - `${showPlots.Voltage != undefined ? `&displayVolt=${showPlots.Voltage}` : ``}` + - `${showPlots.Current != undefined ? `&displayCur=${showPlots.Current}` : ``}` + - `${showPlots.TripCoil != undefined ? `&displayTCE=${showPlots.TripCoil}` : ``}` + - `${showPlots.Digitals != undefined ? `&breakerdigitals=${showPlots.Digitals}` : ``}` + - `${showPlots.Analogs != undefined ? `&displayAnalogs=${showPlots.Analogs}` : ``}` + - `${`&displayAnalytics=${analytics}`}` + - `${`&lpfOrder=${lpf}`}` + - `${`&hpfOrder=${hpf}`}` + - `${`&Trc=${trc}`}` + - `${`&harmonic=${harmonic}`}` + - `${type == 'fft' ? `&startDate=${fftTime[0]}` : ``}` + - `${type == 'fft' ? `&cycles=${cycles}` : ``}` + - `&Meter=${eventInfo.MeterName}` + - `&EventType=${eventInfo.EventName}` - ); - } - - return ( - <> - setHover(item)} eventInfo={eventInfo} width={props.Width}/> -
- {(props.Width < 1568 && props.Width > 1200) || props.Width < 1050 ? - <> - {/* Top Section */} -
    - setHover(item)} lookupInfo={lookupInfo} showAbout={showAbout} - showFFT={showFFT} setShowAbout={(item) => setShowAbout(item)} mouseMode={mouseMode} navigation={navigation} OpenDrawers={props.OpenDrawers} ToggleDrawer={props.ToggleDrawer} - /> -
- {/* Bottom section */} -
    - setHover(item)} mouseMode={mouseMode} navigation={navigation} - OpenDrawers={props.OpenDrawers} ToggleDrawer={props.ToggleDrawer} showFFT={showFFT} lookupInfo={lookupInfo} exportData={(item) => exportData(item)} /> -
- : - <> -
    - {/* Left Section */} - setHover(item)} mouseMode={mouseMode} navigation={navigation} - OpenDrawers={props.OpenDrawers} ToggleDrawer={props.ToggleDrawer} showFFT={showFFT} lookupInfo={lookupInfo} exportData={(item) => exportData(item)} /> - {/* Right section */} - setHover(item)} lookupInfo={lookupInfo} showAbout={showAbout} - showFFT={showFFT} setShowAbout={(item) => setShowAbout(item)} mouseMode={mouseMode} navigation={navigation} OpenDrawers={props.OpenDrawers} ToggleDrawer={props.ToggleDrawer} - /> -
- - } - -
- - - ); - -} - -interface InfoSectionProps { - eventInfo: OpenSee.IEventInfo, - hover: Hover, - setHover: (hover: Hover) => void, - width: number -} - -const InfoSection = (props: InfoSectionProps) => { - return ( - <> -
-
    -
  • props.setHover('Meter')} onMouseLeave={() => props.setHover('None')} data-tooltip={'meter'} data-toggle="tooltip" data-placement="bottom" - style={{ borderLeft: '1px solid #ddd', borderRight: '1px solid #ddd', paddingLeft: '30px', paddingRight: '30px' }}> -
    Meter:
    -
    {props.eventInfo?.MeterName?.split(" ")[0]}
    - -

    {props.eventInfo?.MeterName}

    -
    -
  • -
  • props.setHover('Station')} onMouseLeave={() => props.setHover('None')} data-tooltip={'station'} data-toggle="tooltip" data-placement="bottom" - style={{ borderLeft: '1px solid #ddd', borderRight: '1px solid #ddd', paddingLeft: '30px', paddingRight: '30px' }}> -
    Station:
    -
    {props.eventInfo?.StationName}
    - -

    {props.eventInfo?.StationName}

    -
    -
  • -
  • props.setHover('Asset')} onMouseLeave={() => props.setHover('None')} data-tooltip={'asset'} data-toggle="tooltip" data-placement="bottom" - style={{ borderLeft: '1px solid #ddd', borderRight: '1px solid #ddd', paddingLeft: '30px', paddingRight: '30px' }}> -
    Asset:
    -
    {props.eventInfo?.AssetName?.split(" ")[0]}
    - -

    {props.eventInfo?.AssetName}

    -
    -
  • -
  • props.setHover('EType')} onMouseLeave={() => props.setHover('None')} data-tooltip={'etype'} data-toggle="tooltip" data-placement="bottom" - style={{ borderLeft: '1px solid #ddd', borderRight: '1px solid #ddd', paddingLeft: '15px', paddingRight: '15px' }}> -
    Type:
    -
    {props.eventInfo?.EventName}
    - -

    {props.eventInfo?.EventName}

    -
    -
  • - {props.width > 1695 ? -
  • props.setHover('EInception')} onMouseLeave={() => props.setHover('None')} data-tooltip={'einception'} data-toggle="tooltip" data-placement="bottom" - style={{ borderLeft: '1px solid #ddd', borderRight: '1px solid #ddd', paddingLeft: '15px', paddingRight: '15px', minWidth: "60px", marginRight: "10px" }}> -
    Inception:
    -
    - {moment(props.eventInfo?.Inception).format('YYYY-MM-DD HH:mm:ss.SSS')} -
    - -

    {moment(props.eventInfo?.Inception).format('YYYY-MM-DD HH:mm:ss.SSS')}

    -
    -
  • : null} -
-
- - ) - -} - -const PlotTable = () => { - const showPlots = useAppSelector(SelectDisplayed); - const eventIDs = useAppSelector(SelectEventIDs); - const dispatch = useAppDispatch() - - function tooglePlots(type: OpenSee.graphType) { - let display; - if (type === 'Voltage') - display = showPlots.Voltage; - else if (type === 'Current') - display = showPlots.Current; - else if (type === 'Analogs') - display = showPlots.Analogs; - else if (type === 'Digitals') - display = showPlots.Digitals; - else if (type === 'TripCoil') - display = showPlots.TripCoil; - - if (display) - eventIDs.forEach(id => dispatch(RemovePlot({ DataType: type, EventId: id }))) - else - eventIDs.forEach(id => dispatch(AddPlot({ key: { DataType: type, EventId: id } }))) - } - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - -
- tooglePlots('Voltage')} - checked={showPlots.Voltage} /> - - -
- tooglePlots('Current')} - checked={showPlots.Current} /> - - -
- tooglePlots('Analogs')} - checked={showPlots.Analogs} /> - - -
- tooglePlots('Digitals')} - checked={showPlots.Digitals} /> - - -
- tooglePlots('TripCoil')} - checked={showPlots.TripCoil} /> - - -
- - ) -} - -interface NavigationProps { - lookupInfo: OpenSee.iNextBackLookup, - navigation: OpenSee.EventNavigation, - hover: Hover, - setHover: (hover: Hover) => void, -} - -const Navigation = (props: NavigationProps) => { - const dispatch = useAppDispatch(); - - return ( - <> - {props.lookupInfo ? -
  • -
    -
    - -

    Navigate to Previous Event in the {props.navigation}

    - {props.navigation === "system" && (

    ({(props.lookupInfo.System.m_Item1 != null ? props.lookupInfo.System.m_Item1.StartTime : '')})

    )} - {props.navigation === "station" && (

    ({(props.lookupInfo.Station.m_Item1 != null ? props.lookupInfo.Station.m_Item1.StartTime : '')})

    )} - {props.navigation === "meter" && (

    ({(props.lookupInfo.Meter.m_Item1 != null ? props.lookupInfo.Meter.m_Item1.StartTime : '')})

    )} - {props.navigation === "asset" && (

    ({(props.lookupInfo.System.m_Item1 != null ? props.lookupInfo.System.m_Item1.StartTime : '')})

    )} -
    - {(props.navigation == "system" ? props.setHover('NavLeft')} onMouseLeave={() => props.setHover('None')} data-tooltip={'back-btn'} data-toggle="tooltip" data-placement="bottom" style={{ padding: "0.07rem, 0.25rem, 0.25rem, 0.07rem", fontSize: "21px" }}>< : null)} - {(props.navigation == "station" ? props.setHover('NavLeft')} onMouseLeave={() => props.setHover('None')} data-tooltip={'back-btn'} data-toggle="tooltip" data-placement="bottom" style={{ padding: "0.07rem, 0.25rem, 0.25rem, 0.07rem", fontSize: "21px" }}>< : null)} - {(props.navigation == "meter" ? props.setHover('NavLeft')} onMouseLeave={() => props.setHover('None')} data-tooltip={'back-btn'} data-toggle="tooltip" data-placement="bottom" style={{ padding: "0.07rem, 0.25rem, 0.25rem, 0.07rem", fontSize: "21px" }}>< : null)} - {(props.navigation == "asset" ? props.setHover('NavLeft')} onMouseLeave={() => props.setHover('None')} data-tooltip={'back-btn'} data-toggle="tooltip" data-placement="bottom" style={{ padding: "0.07rem, 0.25rem, 0.25rem, 0.07rem", fontSize: "21px" }}>< : null)} -
    - -
    - -

    Navigate to Next Event in the {props.navigation}

    - {props.navigation === "system" && (

    ({props.lookupInfo.System.m_Item2 != null ? props.lookupInfo.System.m_Item2.StartTime : ''})

    )} - {props.navigation === "station" && (

    ({(props.lookupInfo.Station.m_Item2 != null ? props.lookupInfo.Station.m_Item2.StartTime : '')})

    )} - {props.navigation === "meter" && (

    ({(props.lookupInfo.Meter.m_Item2 != null ? props.lookupInfo.Meter.m_Item2.StartTime : '')})

    )} - {props.navigation === "asset" && (

    ({(props.lookupInfo.Asset.m_Item2 != null ? props.lookupInfo.Asset.m_Item2.StartTime : '')})

    )} -
    - {(props.navigation == "system" ? props.setHover('NavRight')} onMouseLeave={() => props.setHover('None')} data-tooltip={'next-btn'} data-toggle="tooltip" data-placement="bottom" style={{ padding: "0.07rem, 0.25rem, 0.25rem, 0.07rem", fontSize: "21px" }}>> : null)} - {(props.navigation == "station" ? props.setHover('NavRight')} onMouseLeave={() => props.setHover('None')} data-tooltip={'next-btn'} data-toggle="tooltip" data-placement="bottom" style={{ padding: "0.07rem, 0.25rem, 0.25rem, 0.07rem", fontSize: "21px" }}>> : null)} - {(props.navigation == "meter" ? props.setHover('NavRight')} onMouseLeave={() => props.setHover('None')} data-tooltip={'next-btn'} data-toggle="tooltip" data-placement="bottom" style={{ padding: "0.07rem, 0.25rem, 0.25rem, 0.07rem", fontSize: "21px" }}>> : null)} - {(props.navigation == "asset" ? props.setHover('NavRight')} onMouseLeave={() => props.setHover('None')} data-tooltip={'next-btn'} data-toggle="tooltip" data-placement="bottom" style={{ padding: "0.07rem, 0.25rem, 0.25rem, 0.07rem", fontSize: "21px" }}>> : null)} -
    -
    -
  • : null} - - ) -} - -interface iPlotUtilities { - hover: Hover, - setHover: (hover: Hover) => void, - mouseMode: OpenSee.MouseMode, - OpenDrawers: OpenSee.Drawers, - navigation: OpenSee.EventNavigation, - showAbout: boolean, - showFFT: boolean, - lookupInfo: OpenSee.iNextBackLookup, - ToggleDrawer: (drawer: OpenSee.OverlayDrawers, open: boolean) => void, - setShowAbout: (about: boolean) => void -} - -const PlotUtilitiesSection = (props: iPlotUtilities) => { - const dispatch = useAppDispatch(); - - return ( - <> -
  • -
    - {/*Zoom*/} - - - - -

    Zoom

    -
    - - {/*Pan*/} - - -

    Pan

    -
    - - { /*Select*/} - - -

    Select

    -
    - - {/*FFT Move*/} - - -

    FFT Move

    -
    - - {/*reset*/} - - -

    Reset Zoom

    -
    - -
    -
  • - -
  • - - -

    Settings

    -
    -
  • - - props.setHover(item)} lookupInfo={props.lookupInfo} navigation={props.navigation} /> - -
  • - - -

    Help

    -
    - props.setShowAbout(false)} /> - -
  • - - ) -} - -interface iWidgets { - hover: Hover, - setHover: (hover: Hover) => void, - mouseMode: OpenSee.MouseMode, - OpenDrawers: OpenSee.Drawers, - navigation: OpenSee.EventNavigation, - showFFT: boolean, - lookupInfo: OpenSee.iNextBackLookup, - ToggleDrawer: (drawer: OpenSee.OverlayDrawers, open: boolean) => void, - exportData: (type: string) => void, - eventInfo: OpenSee.IEventInfo -} - -const WidgetSection = (props: iWidgets) => { - const dispatch = useAppDispatch(); - - return ( - <> -
  • -
  • -
  • - - -
    - -
    - -

    Waveform Views

    -
    -
  • - -
  • - - -

    Show Points

    -
    -
  • - -
  • - - -

    Phasor Chart

    -
    -
  • -
  • - - - -

    Stats

    -
    -
  • - -
  • - - -

    Correlated Sags

    -
    -
  • - -
  • - - -

    FFT Table

    -
    -
  • - -
  • - - -

    Lightning Data

    -
    -
  • - -
  • - - - -

    Export

    -
    -
  • - - ) -} - -export default OpenSeeNavBar; diff --git a/src/OpenSEE/Scripts/TSX/Context/HoverContext.tsx b/src/OpenSEE/Scripts/TSX/Context/HoverContext.tsx deleted file mode 100644 index a41f795f..00000000 --- a/src/OpenSEE/Scripts/TSX/Context/HoverContext.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// HoverContext.tsx - - -import * as React from 'react'; - -interface HoverContextType { - hover: [number, number]; - setHover: React.Dispatch>; -} - -const defaultState: HoverContextType = { - hover: [0, 0], - setHover: () => { } -}; - -const HoverContext = React.createContext(defaultState); - -export default HoverContext; diff --git a/src/OpenSEE/Scripts/TSX/Context/HoverProvider.tsx b/src/OpenSEE/Scripts/TSX/Context/HoverProvider.tsx deleted file mode 100644 index dd8832fd..00000000 --- a/src/OpenSEE/Scripts/TSX/Context/HoverProvider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// HoverProvider.tsx -import * as React from 'react'; -import HoverContext from './HoverContext'; - -const HoverProvider = ({ children }) => { - const [hover, setHover] = React.useState<[number, number]>([0, 0]); - - return ( - - {children} - - ); -}; - -export default HoverProvider; diff --git a/src/OpenSEE/Scripts/TSX/Graphs/BarChartBase.tsx b/src/OpenSEE/Scripts/TSX/Graphs/BarChartBase.tsx deleted file mode 100644 index b4595c1e..00000000 --- a/src/OpenSEE/Scripts/TSX/Graphs/BarChartBase.tsx +++ /dev/null @@ -1,847 +0,0 @@ -//****************************************************************************************************** -// BarChartBase.tsx - 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 12/15/2020 - C. Lackner -// Generated original version of source code -// -//****************************************************************************************************** - -import * as React from 'react'; -import * as d3 from "d3"; -import { OpenSee } from '../global'; - -import Legend from './LegendBase'; -import { SelectColor, SelectActiveUnit, SelectMouseMode, SelectZoomMode, } from '../store/settingSlice' -import { - SelectData, SelectEnabled, SelectLoading, SelectYLimits, SetZoomedLimits, SelectFFTLimits, - SetFFTLimits, SelectRelevantUnits, getPrimaryAxis, SelectYLabels, SelectEnabledUnits -} from '../store/dataSlice'; -import { SelectAnalyticOptions } from '../store/analyticSlice'; -import { LoadingIcon, NoDataIcon } from './ChartIcons'; -import { useAppDispatch, useAppSelector } from '../hooks'; - -interface iProps { - height: number, - width: number, - dataKey: OpenSee.IGraphProps -} - -// The following Classes are used in this -// xAxis, yaxis => The axis Labels -// xAxisExtLeft, xAxisExtRight => axis extensions because Xaxis stops left and right center Bar -// xAxisLabel, yAxisLabel => The Text next to the Axis -// root => The SVG Container -// bar => The Trace -// active => indicates an active trace -// SelectedPoints => a group of points Selected -// selectedPoint => a single point -// toolTip => The vertical Line used as tooltip -// zoomWindow => Window shown when zooming -// clip => The Clipp Path -// DataContainer => The Container that has all the Databased elements (Line, Marker etc) -// Overlay => The Container Overlayed for eventHandling - -const BarChart = (props: iProps) => { - const dataKey: OpenSee.IGraphProps = { DataType: props.dataKey.DataType, EventId: props.dataKey.EventId }; - const SelectActiveUnitInstance = React.useMemo(() => SelectActiveUnit(dataKey), [props.dataKey.EventId, props.dataKey.DataType]) - const selectAnalyticOptionInstance = React.useMemo(() => SelectAnalyticOptions(props.dataKey.DataType), [props.dataKey.DataType]) - const MemoSelectNumUnits = React.useMemo(() => SelectRelevantUnits(dataKey), []); - const yLimits = useAppSelector(SelectYLimits(dataKey)); - - const MemoSelectData = React.useMemo(() => SelectData(dataKey), []); - const MemoSelecEnable = React.useMemo(() => SelectEnabled(dataKey), []); - - const xScaleRef = React.useRef>(); - const xScaleLblRef = React.useRef(); - const yScaleRef = React.useRef> | {}>({}); - - const [isCreated, setCreated] = React.useState(false); - const [mouseDown, setMouseDown] = React.useState(false); - const [pointMouse, setPointMouse] = React.useState<[number, number]>([0, 0]); - const [mouseDownInit, setMouseDownInit] = React.useState(false); - const relevantUnits = useAppSelector(MemoSelectNumUnits); - const MemoSelectEnabledUnit = React.useMemo(() => SelectEnabledUnits(props.dataKey), []); - const enabledUnits = useAppSelector(MemoSelectEnabledUnit); - - const barData = useAppSelector(MemoSelectData); - const enabledBar = useAppSelector(MemoSelecEnable); - - const yLabels = useAppSelector(SelectYLabels(dataKey)); - - const xLimits = useAppSelector(SelectFFTLimits); - - const loading = useAppSelector(SelectLoading(dataKey)); - - const colors = useAppSelector(SelectColor); - const activeUnit = useAppSelector(SelectActiveUnitInstance); - const mouseMode = useAppSelector(SelectMouseMode); - const zoomMode = useAppSelector(SelectZoomMode); - - const dispatch = useAppDispatch(); - const options = useAppSelector(selectAnalyticOptionInstance) - - const [hover, setHover] = React.useState<[number, number]>([0, 0]); - const [yLblFontSize, setYLblFontSize] = React.useState | {}>({}); - const primaryAxis = getPrimaryAxis(dataKey) - - React.useEffect(() => { - - if (barData && barData?.length > 0 && loading !== 'Loading') { - if (isCreated) - UpdateData(); - - createPlot(); - UpdateData(); - updateVisibility(); - setCreated(true); - } - - }, [barData, loading]); - - //Effect to adjust Axes Labels when Scale changes - React.useEffect(() => { - if (yScaleRef.current != undefined && xScaleRef.current != undefined) - updateSize(); - - }, [props.height, props.width]) - - - React.useEffect(() => { - if (barData && barData?.length > 0) - updateVisibility(); - }, [enabledBar]) - - //Effect to adjust Axes when Units change - React.useEffect(() => { - if (yScaleRef.current == undefined || xScaleRef.current == undefined) - return; - - relevantUnits.forEach(unit => { - if (yScaleRef.current?.[unit] && yLimits?.[unit]) { - yScaleRef.current[unit].domain(yLimits[unit]); - } - }); - - if (barData && barData.length > 0) { - let domain = barData[0].DataPoints.filter(pt => pt[0] >= xLimits[0] && pt[0] <= xLimits[1]).map(pt => pt[0]); - xScaleRef.current.domain(domain); - xScaleLblRef.current.domain([60.0 * domain[0], 60.0 * domain[domain.length - 1]]); - } - - if (yLimits) - updateLimits(); - - - }, [activeUnit, yLimits]) - - - React.useEffect(() => { - updateHover(); - }, [hover]); - - React.useEffect(() => { - if (!mouseDownInit) { - setMouseDownInit(true); - return; - } - - if (!mouseDown && mouseMode == 'zoom' && zoomMode == "x") - dispatch(SetFFTLimits({ end: Math.max(pointMouse[0], hover[0]), start: Math.min(pointMouse[0], hover[0]) })) - else if (!mouseDown && mouseMode == 'zoom' && zoomMode == "y") - dispatch(SetZoomedLimits({ limits: [Math.min(pointMouse[1], hover[1]), Math.max(pointMouse[1], hover[1])], key: dataKey })) - else if (!mouseDown && mouseMode == 'zoom' && zoomMode == "xy") { - dispatch(SetFFTLimits({ end: Math.max(pointMouse[0], hover[0]), start: Math.min(pointMouse[0], hover[0]) })) - dispatch(SetZoomedLimits({ limits: [Math.min(pointMouse[1], hover[1]), Math.max(pointMouse[1], hover[1])], key: dataKey })) - } - }, [mouseDown]) - - - React.useEffect(() => { - updateColors(); - }, [colors]) - - //This Clears the Plot if loading is activated - React.useEffect(() => { - d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId + ">svg").select("g.root").remove() - - if (loading == 'Loading') { - setCreated(false); - return; - } - - if (barData?.length == 0) { - setCreated(false); - return; - } - createPlot(); - UpdateData(); - updateVisibility(); - - return () => { } - - }, [props.dataKey, options]); - - React.useEffect(() => { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - if (container == null || container.select(".yAxisLabel") == null) - return; - - let yLabelLeft = container.select(`.yAxisLabel.left`) - - yLabelLeft.style('font-size', yLblFontSize.toString() + 'rem'); - yLabelLeft.text(yLabels[primaryAxis]) - - relevantUnits.forEach(unit => { - container.select(`.yAxisLabel.right[type='axis-${unit}']`).style('font-size', yLblFontSize.toString() + 'rem'); - container.select(".yAxisLabel.right").text(yLabels[unit]) - }) - - }, [yLabels, yLblFontSize]); - - - React.useLayoutEffect(() => { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - if (container == null || container.select(".yAxisLabel") == null || yLabels[primaryAxis].length == 0) - return; - - relevantUnits.forEach(unit => { - let fs = 1; - let l = GetTextWidth('', '1rem', yLabels[unit]); - - while ((l > props.height - 60) && fs > 0.2) { - fs = fs - 0.05; - l = GetTextWidth('', fs.toString() + 'rem', yLabels[unit]); - } - - if (fs != yLblFontSize[unit]) - setYLblFontSize(prevState => ({ - ...prevState, - [unit]: fs - })); - }) - - }); - - function createLineGen(unit = null, base = null) { - let factor = 1.0 - - // Calculate factor if unit and base are provided - if (unit && base && activeUnit[unit]) { - factor = activeUnit[unit].factor; - if (factor === undefined) //p.u case - factor = 1.0 / base - } - - if (enabledUnits?.length > 2) - xScaleRef.current.range([120, props.width - 110]); - - if (enabledUnits?.length > 3) - xScaleRef.current.range([120, props.width - 170]); - - return d3.line() - .x(d => xScaleRef.current ? (xScaleRef.current(d[0]) + (xScaleRef.current.bandwidth() / 2)) : 0) - .y(d => yScaleRef?.current[unit] ? yScaleRef?.current[unit](d[1] * factor) : 0) - .defined(d => { - let tx = !isNaN(parseFloat(xScaleRef.current ? (xScaleRef.current(d[0]) + (xScaleRef.current.bandwidth() / 2)?.toString()) : '0')); - let ty = !isNaN(parseFloat(yScaleRef?.current[unit] ? yScaleRef.current[unit](d[1] * factor)?.toString() : '0')); - tx = tx && isFinite(parseFloat(xScaleRef.current ? (xScaleRef.current(d[0]) + (xScaleRef.current.bandwidth() / 2))?.toString() : '0')); - ty = ty && isFinite(parseFloat(yScaleRef?.current[unit] ? yScaleRef.current[unit](d[1] * factor)?.toString() : '0')); - return tx && ty; - }); - } - - // This Function needs to be called whenever Data is Added - function UpdateData() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - //draw bars for Mag - const rectData = barData.filter(d => d.LegendHorizontal === "Mag") - - let rectangles = container.select(".DataContainer").selectAll(".Bar") - .data(rectData) - .enter().append("g") - .classed("Bar", true) - .attr("stroke", d => colors[d.Color]) - .selectAll('rect') - .data(d => d.DataPoints.map(pt => { return { unit: d.Unit, data: pt, color: d.Color, base: d.BaseValue, enabled: d.Enabled } }) as OpenSee.BarSeries[]) - .enter() - .append('rect') - .attr("x", d => { - let x = xScaleRef.current(d.data[0]); - return isNaN(x) ? 0 : x - }) - .attr("y", d => { let y = yScaleRef.current[d.unit](d.data[1]); return isNaN(y) ? 0 : y }) - .attr("width", xScaleRef.current.bandwidth()) - .attr("height", d => { - let h = yScaleRef.current[d.unit](d.data[1]) - return isNaN(h) ? 0 : Math.max(((props.height - 60) - yScaleRef.current[d.unit](d.data[1])), 0) - }) - .attr("fill", "none") - .attr("stroke-width", 2) - .style("transition", 'x 0.5s') - .style("transition", 'y 0.5s') - .style("transition", 'width 0.5s') - .style("height", 'width 0.5s') - - - - //draw circles for Ang - const pointData = barData.filter(d => d.LegendHorizontal === "Ang") - let circles = container.select(".DataContainer").selectAll(".Point") - .data(pointData) - .enter().append("g") - .classed("Point", true) - .attr("fill", d => colors[d.Color]) - .selectAll('circle') - .data(d => d.DataPoints.map(pt => { return { unit: d.Unit, data: pt, color: d.Color, base: d.BaseValue, enabled: d.Enabled } }) as OpenSee.BarSeries[]) - .enter().append('circle') - .attr("cx", d => isNaN(xScaleRef.current(d.data[0])) ? -1 : xScaleRef.current(d.data[0])) //set the circle cx position - .attr("cy", d => isNaN(yScaleRef.current[d.unit](d.data[1])) ? -1 : yScaleRef.current[d.unit](d.data[1])) //set the circle cy position - .attr("r", 5) //set the radius as 5 - .attr("stroke", "none") //set the stroke as none - .style("transition", 'cx 0.5s') - .style("transition", 'cy 0.5s') - .style("transition", 'r 0.5s') - - - //draw lines to connect Ang circles - let lines = container.select(".DataContainer").selectAll(".Line").data(pointData); - lines.enter().append("path").classed("Line", true) - .attr("type", d => `axis-${d.Unit}`) - .attr("fill", "none") - .attr("stroke", d => (Object.keys(colors).indexOf(d.Color) > -1 ? colors[d.Color] : colors.random)) - .attr("stroke-dasharray", d => (d.LineType == undefined || d.LineType == "-" ? 0 : 5)) - .attr("d", d => { - let lineGen = createLineGen(d.Unit) - if (d.SmoothDataPoints.length > 0) - return lineGen.curve(d3.curveNatural)(d.SmoothDataPoints); - return lineGen(d.DataPoints); - }) - - lines.exit().remove(); - - - container.select(".DataContainer").selectAll(".Bar").data(rectData).exit().remove(); - container.select(".DataContainer").selectAll(".Point").data(pointData).exit().remove(); - - - updateLimits(); - - } - - // This Function should be called anytime the Scale changes as it will adjust the Axis, Path and Points - function updateLimits() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - container.selectAll(".xAxis").transition().call(d3.axisBottom(xScaleLblRef.current).tickFormat(d => formatFrequencyTick(d as number)).tickSizeOuter(0) as any); - - const offsetLeft = xScaleRef.current.step() * xScaleRef.current.paddingOuter() * xScaleRef.current.align() * 2 + 0.5 * xScaleRef.current.bandwidth(); - const offsetRight = xScaleRef.current.step() * xScaleRef.current.paddingOuter() * (1 - xScaleRef.current.align()) * 2 + 0.5 * xScaleRef.current.bandwidth(); - - xScaleLblRef.current.range([60 + offsetLeft, props.width - 110 - offsetRight]); - container.select('.xAxisExtLeft').attr("x2", 60 + offsetLeft) - container.select('.xAxisExtRight').attr("x2", props.width - 110 - offsetRight) - - let barGen = (unit: OpenSee.Unit, base: number) => { - //Determine Factors - let factor = 1.0; - if (activeUnit?.[unit]) { - factor = activeUnit[unit].factor - if (factor === undefined) //p.u case - factor = 1.0 / base - } - - return (d) => { - return yScaleRef.current[d.unit](d.data[1] * factor) - } - } - - container.select(".DataContainer").selectAll(".Bar").selectAll('rect') - .attr("x", (d: OpenSee.BarSeries) => { let v = xScaleRef.current(d.data[0]); return (isNaN(v) ? 0.0 : v) }) - .style("opacity", (d: OpenSee.BarSeries) => { let v = xScaleRef.current(d.data[0]); return (isNaN(v) ? 0.0 : 1.0) }) - .attr("y", (d: OpenSee.BarSeries) => { let y = barGen(d.unit, d.base)(d); return (isNaN(y) ? 0 : y) }) - .attr("width", Math.max(xScaleRef.current.bandwidth())) - .attr("height", (d: OpenSee.BarSeries) => { - let h = barGen(d.unit, d.base)(d) - if (isNaN(h)) - return 0 - return Math.max(((props.height - 40) - barGen(d.unit, d.base)(d)), 0) - }) - - container.select(".DataContainer").selectAll(".Point").selectAll('circle') - .attr("cx", (d: OpenSee.BarSeries) => { let v = (xScaleRef.current(d.data[0])) + (xScaleRef.current.bandwidth() / 2); return (isNaN(v) ? 0 : v) }) - .style("opacity", (d: OpenSee.BarSeries) => { let v = xScaleRef.current(d.data[0]); return (isNaN(v) ? 0.0 : 1.0) }) - .attr("cy", (d: OpenSee.BarSeries) => (isNaN(yScaleRef.current[d.unit](d.data[0])) ? -1 : (barGen(d.unit, d.base)(d)))) - .attr("r", (d: OpenSee.BarSeries) => { let v = xScaleRef.current(d.data[0]); return (isNaN(v) ? 0.0 : 5) }) - - updateYAxises(); - - } - - - function createPlot() { - d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId + ">svg").select("g.root").remove() - - let svg = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId).select("svg") - .append("g").classed("root", true) - .attr("transform", "translate(10,0)"); - - // Set yScales - if (yLimits) { - Object.keys(yLimits).forEach(unit => { - if (yLimits?.[unit]) - yScaleRef.current[unit] = d3.scaleLinear().domain(yLimits?.[unit]).range([props.height - 40, 20]); - else - yScaleRef.current[unit] = d3.scaleLinear().domain([0, 1]).range([props.height - 40, 20]); - }) - } - - // We can assume consistent sampling rate for now - let domain = barData[0].DataPoints.filter(pt => pt[0] >= xLimits[0] && pt[0] <= xLimits[1]).map(pt => pt[0]); - xScaleRef.current = d3.scaleBand(domain, [60, props.width - 150]) - - const offsetLeft = xScaleRef.current.step() * xScaleRef.current.paddingOuter() * xScaleRef.current.align() * 2 + 0.5 * xScaleRef.current.bandwidth(); - const offsetRight = xScaleRef.current.step() * xScaleRef.current.paddingOuter() * (1 - xScaleRef.current.align()) * 2 + 0.5 * xScaleRef.current.bandwidth(); - - xScaleLblRef.current = d3.scaleLinear().domain([(domain[0] * 60.0), (domain[domain.length - 1] * 60.0)]).range([60 + offsetLeft, props.width - 110 - offsetRight]); - - //create xAxis - svg.append("g").classed("xAxis", true).attr("transform", "translate(0," + (props.height - 40) + ")").call(() => d3.axisBottom(xScaleRef.current).tickFormat((d, i) => formatFrequencyTick(d as number)).tickSize(6)); - - let isAxisLeft = true; - let axisCount = 0; - - //Create yAxises - relevantUnits.forEach(unit => { - let axisTransform = isAxisLeft ? "translate(60,0)" : `translate(${props.width - 110},0)`; - const enabledUnit = enabledUnits.includes(unit) - - svg.append("g") - .classed(`yAxis`, true) - .attr("type", `${unit}`) - .attr("transform", axisTransform) - .call(isAxisLeft ? d3.axisLeft(yScaleRef.current[unit]).tickFormat(d => formatValueTick(d as number, unit)) : d3.axisRight(yScaleRef.current[unit]).tickFormat(d => formatValueTick(d as number, unit))) - .style("opacity", enabledUnit ? 1 : 0) - - // Create axis label - let labelYPos = isAxisLeft ? 2 : props.width - 70; - - svg.append("text") - .classed(isAxisLeft ? `yAxisLabelLeft` : `yAxisLabelRight`, true) - .attr("type", `${unit}`) - .attr("x", - (props.height / 2 - 20)) - .attr("y", labelYPos) - .attr("dy", "1em") - .attr("transform", "rotate(-90)") - .style("text-anchor", "middle") - .style("opacity", enabledUnit ? 1 : 0) - .text(yLabels[unit]); - - isAxisLeft = !isAxisLeft; - axisCount++; - - }); - //Create Axis Labels - svg.append("text").classed("xAxisLabel", true) - .attr("transform", "translate(" + ((props.width - 210) / 2 + 60) + " ," + (props.height - 10) + ")") - .style("text-anchor", "middle") - .text('Harmonic (Hz)'); - - // Create Plot Title - svg.append("text").classed("plotTitle", true) - .attr("transform", "translate(" + ((props.width - 210) / 2 + 60) + ", 20)") - .style("text-anchor", "middle") - .style("font-weight", "bold") - .text(dataKey.DataType); - - svg.append("line").classed("xAxisExtLeft", true) - .attr("stroke", "currentColor") - .attr("x1", 60).attr("x2", 60 + offsetLeft) - .attr("y1", props.height - 40).attr("y2", props.height - 40) - - svg.append("line").classed("xAxisExtRight", true) - .attr("stroke", "currentColor") - .attr("x1", props.width - 110).attr("x2", props.width - 110 - offsetRight) - .attr("y1", props.height - 40).attr("y2", props.height - 40) - - - //Add Clip Path - svg.append("defs").append("svg:clipPath") - .attr("id", "clip-" + props.dataKey.DataType + "-" + props.dataKey.EventId) - .append("svg:rect").classed("clip", true) - .attr("width", props.width - 170) - .attr("height", props.height - 60) - .attr("x", 60) - .attr("y", 20); - - - //Add Window to indicate Zooming - svg.append("rect").classed("zoomWindow", true) - .attr("stroke", "#000") - .attr("x", 60).attr("width", 0) - .attr("y", 20).attr("height", props.height - 40) - .attr("fill", "black") - .style("opacity", 0); - - - //Add Empty group for Data Points - svg.append("g").classed("DataContainer", true) - .attr("clip-path", "url(#clip-" + props.dataKey.DataType + "-" + props.dataKey.EventId + ")"); - - //Event overlay - svg.append("svg:rect").classed("Overlay", true) - .attr("width", props.width - 110) - .attr("height", '100%') - .attr("x", 20) - .attr("y", 0) - .style("opacity", 0) - .on('mousemove', MouseMove) - .on('mouseout', MouseOut) - .on('mousedown', MouseDown) - .on('mouseup', MouseUp) - } - - - function formatValueTick(d: number, unit) { - let h = 1; - - if (yScaleRef.current != undefined) - h = yScaleRef.current[unit].domain()[1] - yScaleRef.current[unit].domain()[0] - - if (h > 100) - return d.toFixed(0) - - if (h > 10) - return d.toFixed(1) - if (h > 1) - return d.toFixed(2) - else - return d.toFixed(3) - - } - - function formatFrequencyTick(d: number) { - let h = 1; - - if (xScaleLblRef.current != undefined) - h = xScaleLblRef.current.domain()[1] - xScaleLblRef.current.domain()[0] - - if (h > 100) - return d.toFixed(0) - - if (h > 10) - return d.toFixed(1) - if (h > 1) - return d.toFixed(2) - else - return d.toFixed(3) - - } - - function MouseMove(evt) { - - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - let x0 = d3.pointer(evt, container.select(".Overlay").node())[0]; - let y0 = d3.pointer(evt, container.select(".Overlay").node())[1]; - let t0 = getXbucket(x0); - let d0 = (yScaleRef.current[primaryAxis] as any).invert(y0); - setHover([t0, d0]) - } - - function MouseDown(evt) { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - let x0 = d3.pointer(evt, container.select(".Overlay").node())[0]; - let y0 = d3.pointer(evt, container.select(".Overlay").node())[1]; - - let t0 = getXbucket(x0); - let d0 = (yScaleRef.current[primaryAxis] as any).invert(y0); - - setMouseDown(true); - setPointMouse([t0, d0]); - - } - - function MouseUp() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - setMouseDown(false); - container.select(".zoomWindow").style("opacity", 0); - - } - - function getXbucket(pixel: number) { - - let scaleSize = xScaleRef.current.range(); - let p = pixel - scaleSize[0]; - - let eachBand = xScaleRef.current.step(); - - let index = Math.floor((p / eachBand)); - if (index == xScaleRef.current.domain().length) - index = index - 1 - - return xScaleRef.current.domain()[index]; - } - - // This function needs to be called if hover is updated - function updateHover() { - - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - if (mouseMode == 'zoom' && mouseDown) { - if (zoomMode == "x") - container.select(".zoomWindow").style("opacity", 0.5) - .attr("x", (xScaleRef.current as any)(Math.min(hover[0], pointMouse[0])) + 0.5 * (xScaleRef.current.bandwidth())) - .attr("width", Math.abs((xScaleRef.current as any)(hover[0]) - (xScaleRef.current as any)(pointMouse[0]))) - .attr("height", props.height - 60) - .attr("y", 20) - else if (zoomMode == "y") - container.select(".zoomWindow").style("opacity", 0.5) - .attr("x", (xScaleRef.current as any)(xLimits[0])) - .attr("width", (xScaleRef.current as any)(xLimits[1]) - (xScaleRef.current as any)(xLimits[0])) - .attr("height", Math.abs((yScaleRef.current[primaryAxis] as any)(pointMouse[1]) - (yScaleRef.current[primaryAxis] as any)(hover[1]))) - .attr("y", Math.min((yScaleRef.current[primaryAxis] as any)(pointMouse[1]), (yScaleRef.current[primaryAxis] as any)(hover[1]))) - else if (zoomMode == "xy") - container.select(".zoomWindow").style("opacity", 0.5) - .attr("x", (xScaleRef.current as any)(Math.min(hover[0], pointMouse[0]))) - .attr("width", Math.abs((xScaleRef.current as any)(hover[0]) - (xScaleRef.current as any)(pointMouse[0]))) - .attr("height", Math.abs((yScaleRef.current[primaryAxis] as any)(pointMouse[1]) - (yScaleRef.current[primaryAxis] as any)(hover[1]))) - .attr("y", Math.min((yScaleRef.current[primaryAxis] as any)(pointMouse[1]), (yScaleRef.current[primaryAxis] as any)(hover[1]))) - } - - let deltaT = hover[0] - pointMouse[0]; - let deltaData = hover[1] - pointMouse[1]; - - if (mouseMode == 'pan' && mouseDown && (zoomMode == "x" || zoomMode == "xy") && Math.abs(deltaT) > 0) - dispatch(SetFFTLimits({ start: (xLimits[0] - deltaT), end: (xLimits[1] - deltaT) })); - - if (mouseMode == 'pan' && mouseDown && (zoomMode == "y" || zoomMode == "xy")) - dispatch(SetZoomedLimits({ limits: [(yLimits[primaryAxis][0] - deltaData), (yLimits[primaryAxis][1] - deltaData)], key: props.dataKey })); - } - - function updateYAxises() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - let svg = container.select(".DataContainer"); - - if (container === undefined) - return - - //Flag to alternate axis placement - let isAxisLeft = true; //this can just be exchanged for a % 2 since we have a counter now. - let currentAxis = 0; - - //Update yAxises - enabledUnits?.forEach(unit => { - let axisType = `[type='${unit}']`; - let firstLeftAxisType = `[type='${enabledUnits[0]}']` - let firstRightAxisType = `[type='${enabledUnits[1]}']` - - if (isAxisLeft) { - container.selectAll(`.yAxis${axisType}`).transition().call(d3.axisLeft(yScaleRef.current[unit]).tickFormat(d => formatValueTick(d as number, unit)) as any); - - if (currentAxis > 1) { - container.selectAll(`.yAxis${firstLeftAxisType}`).attr("transform", "translate(120, 0)") - container.selectAll(`.yAxisLabelLeft${firstLeftAxisType}`).attr("y", "62") - } - } - else { - if (currentAxis > 2) { - container.selectAll(`.yAxis${firstRightAxisType}`).attr("transform", `translate(${props.width - 170},0)`) - container.selectAll(`.yAxisLabelRight${firstRightAxisType}`).attr("y", props.width - 135) - } - container.selectAll(`.yAxis`).selectAll(`[type='${unit}']`).transition().call(d3.axisRight(yScaleRef.current[unit]).tickFormat(d => formatValueTick(d as number, unit)) as any); - svg.selectAll(`path[type='axis-${unit}']`) - .attr("d", function (d: OpenSee.iD3DataSeries) { - const scopedLineGen = createLineGen(d.Unit, d.BaseValue); - if (d.SmoothDataPoints.length > 0) - return scopedLineGen.curve(d3.curveNatural)(d.SmoothDataPoints); - return scopedLineGen(d.DataPoints); - }) - } - - isAxisLeft = !isAxisLeft; - currentAxis++; - - - }); - - - if (enabledUnits.length < 3) - return - - let clipPath = container.select(`#clipData-${props.dataKey.DataType}-${props.dataKey.EventId} > rect`) - let evtOverlay = container.select(`rect.Overlay`) - - if (enabledUnits.length === 3) { - clipPath.attr("x", 120).attr("width", props.width - 270) - evtOverlay.attr("x", 120).attr("width", props.width - 270) - } - else if (enabledUnits.length === 4) { - clipPath.attr("x", 120).attr("width", props.width - 210 - 120) - evtOverlay.attr("x", 120).attr("width", props.width - 210 - 120) - } - - } - - function MouseOut() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - container.select(".zoomWindow").style("opacity", 0); - setMouseDown(false); - } - - - //This Function needs to be called whenever (a) Color Setting changes occur - function updateColors() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - function GetColor(col: OpenSee.Color) { - return colors[col as string] - } - - container.select(".DataContainer").selectAll(".Bar").attr("fill", (d: OpenSee.iD3DataSeries) => GetColor(d.Color)); - - } - - - //This Function needs to be called whenever a item is selected or deselected in the Legend - function updateVisibility() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - if (barData) { - //.transition().duration(1000) leads to a performance issue. need to investigate how to avoid this - const rectData = barData.filter(d => d.LegendHorizontal === "Mag") - container.select(".DataContainer").selectAll(".Bar").data(rectData).classed("active", d => d.Enabled) - container.select(".DataContainer").selectAll(".Bar.active").style("opacity", 1.0); - container.select(".DataContainer").selectAll(".Bar:not(.active)").style("opacity", 0); - - const pointData = barData.filter(d => d.LegendHorizontal === "Ang") - - container.select(".DataContainer").selectAll(".Point").data(pointData).classed("active", d => d.Enabled) - container.select(".DataContainer").selectAll(".Line").data(pointData).classed("active", d => d.Enabled) - container.select(".DataContainer").selectAll(".Line.active").style("opacity", 1.0); - container.select(".DataContainer").selectAll(".Line:not(.active)").style("opacity", 0); - - - container.select(".DataContainer").selectAll(".Point.active").style("opacity", 1.0); - container.select(".DataContainer").selectAll(".Point:not(.active)").style("opacity", 0); - - let isAxisLeft = true; - - relevantUnits.forEach(unit => { - let enabledUnit = enabledUnits?.includes(unit); - let axisType = `[type='${unit}']`; - - if (enabledUnit) { - container.selectAll(`.yAxis${axisType}`).style("opacity", 1); - container.selectAll(`.yAxisLabel${axisType}`).style("opacity", 1); - } else { - container.selectAll(`.yAxis${axisType}`).remove(); - container.selectAll(`.yAxisLabel${axisType}`).remove(); - } - - isAxisLeft = !isAxisLeft; - }) - - } - - } - - - // This Function needs to be called whenever height or width change - function updateSize() { - - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - container.select(".xAxis").attr("transform", "translate(0," + (props.height - 40) + ")"); - - container.select(".xAxisLabel").attr("transform", "translate(" + ((props.width - 210) / 2 + 60) + " ," + (props.height - 5) + ")") - container.select(".plotTitle").attr("transform", "translate(" + ((props.width - 210) / 2 + 60) + ",20)").style("font-weight", "bold") - - container.select(".yAxisLabelLeft").attr("x", - (props.height / 2 - 20)) - container.select(".yAxisLabelRight").attr("y", props.width - 120).attr("x", - (props.height / 2 - 20)) - xScaleRef.current.range([60, props.width - 110]); - container.select(".yAxisRight").attr("transform", "translate(" + (props.width - 150) + ",0)"); - - const offsetLeft = xScaleRef.current.step() * xScaleRef.current.paddingOuter() * xScaleRef.current.align() * 2 + 0.5 * xScaleRef.current.bandwidth(); - const offsetRight = xScaleRef.current.step() * xScaleRef.current.paddingOuter() * (1 - xScaleRef.current.align()) * 2 + 0.5 * xScaleRef.current.bandwidth(); - - xScaleLblRef.current.range([60 + offsetLeft, props.width - 110 - offsetRight]); - - container.select('.xAxisExtLeft') - .attr("x2", 60 + offsetLeft) - .attr("y1", props.height - 40) - .attr("y2", props.height - 40) - - container.select('.xAxisExtRight') - .attr("x2", props.width - 140 - offsetRight) - .attr("y1", props.height - 40) - .attr("y2", props.height - 40) - .attr("x1", props.width - 140) - - relevantUnits.forEach(unit => { - yScaleRef.current[unit].range([props.height - 40, 20]); - }) - - container.select(".clip").attr("height", props.height - 60).attr("width", props.width - 210) - container.select(".Overlay").attr("width", props.width - 210) - - updateLimits(); - } - - - // Helper Function - function GetTextWidth(font: string, fontSize: string, word: string): number { - - const text = document.createElement("span"); - document.body.appendChild(text); - - text.style.font = font; - text.style.fontSize = fontSize; - text.style.height = 'auto'; - text.style.width = 'auto'; - text.style.position = 'absolute'; - text.style.whiteSpace = 'no-wrap'; - text.innerHTML = word; - - const width = Math.ceil(text.clientWidth); - document.body.removeChild(text); - return width; - } - - return ( -
    - 0} hasTrace={enabledBar.some(i => i)} /> - {loading == 'Loading' || barData?.length == 0 ? null : } -
    - ); -} - -const Container = React.memo((props: { height: number, eventID: number, type: OpenSee.graphType, loading: OpenSee.LoadingState, hasData: boolean, hasTrace: boolean }) => { - const showSVG = props.loading != 'Loading' && props.hasData; - - return ( -
    - {props.loading == 'Loading' ? : null} - {props.loading != 'Loading' && !props.hasData ? : null} - - {props.loading != 'Loading' && props.hasData && !props.hasTrace ? - Select a Trace in the Legend to Display. : null} - -
    ) -}) - -export default BarChart; - diff --git a/src/OpenSEE/Scripts/TSX/Graphs/LegendBase.tsx b/src/OpenSEE/Scripts/TSX/Graphs/LegendBase.tsx deleted file mode 100644 index 438353e2..00000000 --- a/src/OpenSEE/Scripts/TSX/Graphs/LegendBase.tsx +++ /dev/null @@ -1,485 +0,0 @@ -//****************************************************************************************************** -// LegendBase.tsx - 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 01/06/2020 - C Lackner -// Generated original version of source code. -// 07/08/2020 - C Lackner -// Refactored Trace Picker to work as Grid. -// -//****************************************************************************************************** -import * as React from "react"; -import { OpenSee } from '../global'; -import { cloneDeep } from "lodash"; -import { SelectData, SelectEnabled, EnableTrace } from "../store/dataSlice"; -import { SelectColor } from "../store/settingSlice"; -import { useAppDispatch, useAppSelector } from "../hooks"; -import { OverlayDrawer } from "@gpa-gemstone/react-interactive"; -import { MultiCheckBoxSelect, StylableSelect } from "@gpa-gemstone/react-forms"; - -const hrow = 26; - -interface iProps { - height: number, - dataKey: OpenSee.IGraphProps -} - -interface ICategory { - Value: number; - Text: string; - Selected: boolean -} - -interface ILegendGrid { - enabled: boolean, - hLabel: string, - vLabel: string, - color: OpenSee.Color, - traces: Map>, - category?: string, -} - -const horizontalSort = ['W', 'Pk', 'RMS', 'Ph', 'V', 'I', 'Pre', 'Post', 'P', 'Q', 'S', 'Pf', 'R', 'X', 'Z', 'Mag', 'Ang']; -const verticalGroupSort = ['L-N', 'L-L', 'Volt.', 'Curr.', 'V', 'I']; -const verticalSort = ['AN', 'BN', 'CN', 'NG', 'RES', 'AB', 'BC', 'CA', 'Avg', 'Total', 'Pos', 'Neg', 'Zero', 'S0/S1', 'S2/S1', 'Simple', 'Reactance', 'Takagi', 'ModifiedTakagi', 'Novosel']; - -const Legend = (props: iProps) => { - const MemoSelectData = React.useMemo(() => SelectData(props.dataKey), []); - const MemoSelectEnabled = React.useMemo(() => SelectEnabled(props.dataKey), []); - - const data = useAppSelector(MemoSelectData); - const enabled = useAppSelector(MemoSelectEnabled) - const dispatch = useAppDispatch(); - - const [categories, setCategories] = React.useState>([]); - const [verticalHeader, setVerticalHeader] = React.useState>([]); - const [horizontalHeader, setHorizontalHeader] = React.useState>([]); - const [grid, setGrid] = React.useState>(new Map()); - const [wScroll, setWScroll] = React.useState(0); - - React.useEffect(() => { - const timeoutId = setTimeout(() => { - dataUpdate(); - }, 250); - - return () => clearTimeout(timeoutId); - }, [data]); - - React.useEffect(() => { - const timeoutId = setTimeout(() => { - update(); - }, 250); - - return () => clearTimeout(timeoutId); - }, [enabled]); - - - React.useEffect(() => { setWScroll(measureScrollbarWidth()); }, []) - function dataUpdate() { - let categories: Array = []; - let grid: Array = []; - - data?.forEach((item: OpenSee.iD3DataSeries, dataIndex) => { - let index = categories.findIndex(category => category.Text === item.LegendGroup); - if (index === -1) { - categories.push({ Value: 0, Text: item.LegendGroup, Selected: false }); - index = categories.findIndex(category => category.Text === item.LegendGroup); - } - if (enabled[dataIndex]) - categories[index].Selected = true; - index = grid.findIndex(g => g.hLabel === item.LegendHorizontal && g.vLabel === item.LegendVertical && g.category == item.LegendVGroup); - if (index === -1) { - grid.push({ enabled: false, hLabel: item.LegendHorizontal, vLabel: item.LegendVertical, color: item.Color, traces: new Map>(), category: item.LegendVGroup }) - index = grid.findIndex(g => g.hLabel === item.LegendHorizontal && g.vLabel === item.LegendVertical && g.category == item.LegendVGroup); - } - if (enabled[dataIndex]) - grid[index].enabled = true; - - if (grid[index].traces.has(item.LegendGroup)) - grid[index].traces.get(item.LegendGroup).push(dataIndex) - else - grid[index].traces.set(item.LegendGroup, [dataIndex]) - }); - - if (categories.length == 1) - categories[0].Selected = true - else if (categories.length > 1) { - if (!categories.some(item => item.Selected)) - categories[0].Selected = true - } - - setGrid(groupBy(grid, item => (item.vLabel + item.category))); - setCategories(categories); - setVerticalHeader(uniq(grid.map(item => [item.vLabel, item.category]), (item) => { return (item[0] + item[1]) }).sort(sortVertical)) - setHorizontalHeader(uniq(grid.map(item => item.hLabel), (d) => d).sort(sortHorizontal)); - } - - function update() { - let updateGrid: Map = cloneDeep(grid); - - data?.forEach((item: OpenSee.iD3DataSeries, dataIndex) => { - - let index = item.LegendVertical + item.LegendVGroup; - if (!updateGrid.has(index)) { - updateGrid.set(index, [{ enabled: false, hLabel: item.LegendHorizontal, vLabel: item.LegendVertical, color: item.Color, traces: new Map>(), category: item.LegendVGroup }]) - } - let dIndex = updateGrid.get(index).findIndex(g => g.hLabel === item.LegendHorizontal) - - if (dIndex == -1) { - updateGrid.set(index, [...updateGrid.get(index), { enabled: false, hLabel: item.LegendHorizontal, vLabel: item.LegendVertical, color: item.Color, traces: new Map>(), category: item.LegendVGroup }]) - dIndex = updateGrid.get(index).findIndex(g => g.hLabel === item.LegendHorizontal); - } - if (enabled[dataIndex]) - updateGrid.get(index)[dIndex].enabled = true; - - if (updateGrid.get(index)[dIndex].traces.has(item.LegendGroup)) { - let ugrid = updateGrid.get(index); - ugrid[dIndex].traces.get(item.LegendGroup).push(dataIndex) - updateGrid.set(index, ugrid); - } - else { - let ugrid = updateGrid.get(index); - ugrid[dIndex].traces.set(item.LegendGroup, [dataIndex]) - updateGrid.set(index, ugrid); - } - }); - - setGrid(updateGrid); - - } - - function sortHorizontal(item1: string, item2: string): number { - if (item1 == item2) - return 0 - - let index1 = horizontalSort.findIndex((v) => v == item1); - let index2 = horizontalSort.findIndex((v) => v == item2); - - if (index1 != -1 && index2 != -1) - return (index1 > index2 ? 1 : -1); - if (index1 != -1) - return 1; - if (index2 != -1) - return -1; - - return (item1 > item2 ? 1 : -1); - } - - function sortVertical(item1: [string, string], item2: [string, string]): number { - if (item1[1] != item2[1]) - return sortGroup(item1, item2); - if (item1[0] == item2[0]) - return 0; - - let index1 = verticalSort.findIndex((v) => v == item1[0]); - let index2 = verticalSort.findIndex((v) => v == item2[0]); - - if (index1 != -1 && index2 != -1) - return (index1 > index2 ? 1 : -1); - if (index1 != -1) - return 1; - if (index2 != -1) - return -1; - - return (item1[0] > item2[0] ? 1 : -1); - } - - function sortGroup(item1: [string, string], item2: [string, string]): number { - if (item1[1] == item2[1]) - return 0 - - let index1 = verticalGroupSort.findIndex((v) => v == item1[1]); - let index2 = verticalGroupSort.findIndex((v) => v == item2[1]); - - if (index1 != -1 && index2 != -1) - return (index1 > index2 ? 1 : -1); - if (index1 != -1) - return 1; - if (index2 != -1) - return -1; - - return (item1[1] > item2[1] ? 1 : -1); - } - - function changeCategory(index: number, item: ICategory) { - - setCategories((current) => { - const tmp = cloneDeep(current); - tmp[index].Selected = !tmp[index].Selected; - - // Also Disable or enable associated Traces and corresponding Grid entries.... - let traces: Array = []; - - if (tmp[index].Selected) - grid.forEach(row => row.forEach(data => { - if (data.traces.has(item.Text) && data.enabled) - traces = traces.concat(data.traces.get(item.Text)); - })); - else - grid.forEach(row => row.forEach(data => { - if (data.traces.has(item.Text)) - traces = traces.concat(data.traces.get(item.Text)); - })); - - dispatch(EnableTrace({ trace: traces, enabled: tmp[index].Selected, key: props.dataKey })); - return tmp; - }); - } - - function uniq(array, fx) { - const result = []; - const resultfx = []; - array.forEach(item => { - const fxn = fx(item); - const index = resultfx.findIndex(sitem => sitem === fxn) - if (index < 0) { - result.push(item); - resultfx.push(fxn); - } - }) - - return result; - } - - function groupBy(list: Array, fnct: (val: ILegendGrid) => string) { - let result: Map = new Map(); - - list.forEach(item => { - if (result.has(fnct(item))) - result.get(fnct(item)).push(item); - else - result.set(fnct(item), [item]); - }) - return result; - } - - - const hwidth = (200 - 4) / (horizontalHeader.length + (verticalHeader.length > 1 ? 2 : 1)); - - function clickGroup(group: string, type: 'vertical' | 'horizontal') { - let isAny = false; - let updates: number[] = []; - - if (type == 'vertical') { - isAny = grid.get(group).some(item => item.enabled); - - - if (isAny) { - grid.get(group).forEach(row => { - if (row.enabled) { - row.enabled = false; - categories.forEach((cat) => { - updates.push(...row.traces.get(cat.Text)); - }); - } - }); - } - else { - - grid.get(group).forEach(row => { - row.enabled = true; - categories.forEach((cat) => { - if (cat.Selected) - updates.push(...row.traces.get(cat.Text)); - }); - - }); - } - } - else { - isAny = false; - grid.forEach(row => { if (row.some(item => (item.enabled && item.hLabel == group))) isAny = true; }); - - if (isAny) { - grid.forEach(row => { - row.forEach(item => { - if (item.enabled && item.hLabel == group) { - item.enabled = false; - categories.forEach((cat) => { - updates.push(...item.traces.get(cat.Text)); - }); - } - }) - }); - } - else { - grid.forEach(row => { - row.forEach(item => { - if (item.hLabel == group) { - item.enabled = true; - categories.forEach((cat) => { - if (cat.Selected) - updates.push(...item.traces.get(cat.Text)); - }); - } - }) - }); - } - } - - dispatch(EnableTrace({ key: props.dataKey, trace: updates, enabled: !isAny })) - - } - const isScroll = (props.height - 97) < (verticalHeader.length * (2 + hrow)); - return ( - -
    -
    - { - options.forEach(o => { - const i = categories.findIndex(c => c.Text == o.Text); - changeCategory(i, categories[i]) - }) - }} - Label={""} - /> -
    -
    -
    -
    1 ? 2 : 1) * hwidth), backgroundColor: "#b2b2b2" }}>
    - {horizontalHeader.map((item, index) =>
    clickGroup(grp, type)} />)} -
    -
    - {(verticalHeader.length > 1 && verticalHeader.some(item => item[1]) ? -
    - {uniq(verticalHeader, v => v[1]).sort(sortGroup).map((value, index) => item[1] == value[1]).length} width={hwidth} />)} -
    : null)} -
    1 && verticalHeader.some(item => item[1]) ? "calc(100% - " + hwidth + "px)" : "100%"), backgroundColor: "rgb(204,204,204)", overflow: "hidden", textAlign: "center", display: "inline-block", verticalAlign: "top" }}> - {verticalHeader.map((value, index) => { - return ( - item.Selected).map(item => item.Text)} - key={index} - label={value[0]} - data={grid?.get(value[0] + value[1])?.sort((item1, item2) => sortHorizontal(item1.hLabel, item2.hLabel))} - width={hwidth} - clickHeader={(grp: string, type: ("vertical" | "horizontal")) => clickGroup(grp, type)} - verticalHeaders={verticalHeader} - horizontalHeaders={horizontalHeader} - /> - ); - })} -
    -
    -
    -
    -
    - ); - -} - -const Category = (props: { key: number, label: string, enabled: boolean, onclick: () => void }) => { - return ( - props.onclick()}>{props.label} - ); -}; - -const Header = (props: { index: number, label: string, width: number, onClick: (str: string, type: string) => void }) => { - - return (
    - props.onClick(props.label, 'horizontal')} > {props.label} -
    ) - -} - -const VCategory = (props: { key: number, label: string, height: number, width: number }) => { - return ( -
    - {props.label} -
    ) -} - -const Row = (props: { category: string, label: string, data: Array, width: number, activeCategories: Array, clickHeader: (group: string, type: string) => void, dataKey: OpenSee.IGraphProps, horizontalHeaders, verticalHeaders }) => { - const hasHorizontalHeaders = props.horizontalHeaders.some(item => item) - const hasCategoryGroup = props.category !== '' && props.category !== null - const labelWidth = !hasHorizontalHeaders && !hasCategoryGroup ? '50%' : hasHorizontalHeaders && !hasCategoryGroup ? 2 * props.width : props.width - - - return ( -
    -
    - props.clickHeader(props.label + props.category, 'vertical')}>{props.label} -
    - {props?.data?.map((item, index) => - ) - } -
    - ) -} - -const TraceButton = (props: { data: ILegendGrid, activeCategory: Array, width: React.CSSProperties, dataKey: OpenSee.IGraphProps }) => { - const colors = useAppSelector(SelectColor) - const dispatch = useAppDispatch(); - - function getColor(color: OpenSee.Color) { - - if (Object.keys(colors).findIndex(key => key == (color as string)) > -1) - return colors[color]; - return colors.random; - } - - function onClick(sender) { - let traces: Array = []; - props.data.traces.forEach(val => { - traces = traces.concat(val); - }) - props.data.enabled = !props.data.enabled; - dispatch(EnableTrace({ key: props.dataKey, trace: traces, enabled: props.data.enabled })); - } - - return ( -
    - {(props.data.enabled ? : )} -
    ) -}; - -function convertHex(hex: string, opacity: number) { - hex = hex.replace("#", ""); - const r = parseInt(hex.substring(0, 2), 16); - const g = parseInt(hex.substring(2, 4), 16); - const b = parseInt(hex.substring(4, 6), 16); - - const result = `rgba(${r},${g},${b},${opacity / 100})`; - return result; -} - -function measureScrollbarWidth(): number { - // Add temporary box to wrapper - let scrollbox = document.createElement('div'); - - // Make box scrollable - scrollbox.style.overflow = 'scroll'; - - // Append box to document - document.body.appendChild(scrollbox); - - // Measure inner width of box - let scrollBarWidth = scrollbox.offsetWidth - scrollbox.clientWidth; - - // Remove box - document.body.removeChild(scrollbox); - - return scrollBarWidth; -} - - -export default Legend - diff --git a/src/OpenSEE/Scripts/TSX/Graphs/LineChartBase.tsx b/src/OpenSEE/Scripts/TSX/Graphs/LineChartBase.tsx deleted file mode 100644 index 21718797..00000000 --- a/src/OpenSEE/Scripts/TSX/Graphs/LineChartBase.tsx +++ /dev/null @@ -1,1167 +0,0 @@ -//****************************************************************************************************** -// LineChartBase.tsx - 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 01/22/2020 - C. Lackner -// Generated original version of source code -// -//****************************************************************************************************** - -import * as React from 'react'; -import * as d3 from "d3"; -import { OpenSee } from '../global'; - -import moment from "moment" -import Legend from './LegendBase'; -import { GetDisplayLabel } from './Utilities' -import { SelectColor, SelectActiveUnit, SelectTimeUnit, SelectSinglePlot, SelectPlotMarkers, SelectUseOverlappingTime, SelectOverlappingWaveTimeUnit, SelectZoomMode, SelectMouseMode } from '../store/settingSlice' - -import { - SelectData, SelectRelevantUnits, SelectIsZoomed, SelectEnabled, SelectStartTime, - SelectEndTime, SelectLoading, SelectYLimits, SetZoomedLimits, SetSelectPoint, SetTimeLimit, SelectEnabledUnits, - SetCycleLimit, SelectYLabels, SelectDeltaHoverPoints, getPrimaryAxis, SelectCycleLimits -} from '../store/dataSlice'; - -import { SelectEventID, SelectEventInfo } from '../store/eventInfoSlice' - -import { SelectEventList } from '../store/overlappingEventsSlice' - -import { SelectAnalyticOptions, SelectCycles, SelectFFTWindow, SelectShowFFTWindow, SelectAnalytics, UpdateAnalytic } from '../store/analyticSlice'; -import { ErrorIcon, LoadingIcon, NoDataIcon } from './ChartIcons'; -import { useAppDispatch, useAppSelector } from '../hooks'; - -import HoverContext from '../Context/HoverContext' -import { defaultSettings } from '../defaults'; - -interface iProps { - height: number, - width: number, - showToolTip: boolean, - dataKey: OpenSee.IGraphProps -} - -interface IMarker { - x: number, - y: number, - unit: string, - base: number -} - -// The following Classes are used in this -// xAxis, yaxis => The axis Labels -// xAxisLabel, yAxisLabel => The Text next to the Axis -// root => The SVG Container -// line => The Trace -// active => indicates an active trace -// SelectedPoints => a group of points Selected -// selectedPoint => a single point -// toolTip => The vertical Line used as tooltip -// zoomWindow => Window shown when zooming -// clip => The Clipp Path -// DataContainer => The Container that has all the Databased elements (Line, Marker etc) -// Overlay => The Container Overlayed for eventHandling - -const LineChart = (props: iProps) => { - const dispatch = useAppDispatch(); - const cycleLimits = useAppSelector(SelectCycleLimits); - const isOverlappingWaveform = props.dataKey.DataType === "OverlappingWave" - - const MemoSelectActiveUnit = React.useMemo(() => SelectActiveUnit(props.dataKey), [props.dataKey]) - const activeUnit = useAppSelector(MemoSelectActiveUnit); - - const MemoSelectAnalyticOption = React.useMemo(() => SelectAnalyticOptions(props.dataKey.DataType), [props.dataKey]) - const options = useAppSelector(MemoSelectAnalyticOption); - - const MemoSelectStartTime = React.useMemo(() => (SelectStartTime), [props.dataKey]) - const MemoSelectEndTime = React.useMemo(() => (SelectEndTime), [props.dataKey]) - - const MemoSelectData = React.useMemo(() => SelectData(props.dataKey), []); - const lineData = useAppSelector(MemoSelectData); - - const MemoSelectRelevantUnits = React.useMemo(() => SelectRelevantUnits(props.dataKey), []); - const relevantUnits = useAppSelector(MemoSelectRelevantUnits); - - const MemoSelectEnabledUnit = React.useMemo(() => SelectEnabledUnits(props.dataKey), []); - const enabledUnits = useAppSelector(MemoSelectEnabledUnit); - - const MemoSelectEnabled = React.useMemo(() => SelectEnabled(props.dataKey), []); - const enabledLine = useAppSelector(MemoSelectEnabled); - - const SelectYlimits = React.useMemo(() => SelectYLimits(props.dataKey), [props.dataKey, lineData]); - const yLimits = useAppSelector(SelectYlimits); - - const isZoomed = useAppSelector(SelectIsZoomed(props.dataKey)); - - const xScaleRef = React.useRef>(); - const yScaleRef = React.useRef> | {}>({}); - - const primaryAxis = getPrimaryAxis(props.dataKey) - - const [isCreated, setCreated] = React.useState(false); - const [mouseDown, setMouseDown] = React.useState(false); - const [fftMouseDown, setFFTMouseDown] = React.useState(false); - const [mouseDownInit, setMouseDownInit] = React.useState(false); - const [pointMouse, setPointMouse] = React.useState<[number, number]>([0, 0]); - - const [toolTipLocation, setTooltipLocation] = React.useState(10); - const [selectedPointLocation, setSelectedPointLocation] = React.useState(null); - const [inceptionLocation, setInceptionLocation] = React.useState(10); - const [durationLocation, setDurationLocation] = React.useState(10); - - const evtID = useAppSelector(SelectEventID); - const isOriginalEvt = props.dataKey.EventId === evtID - - const singlePlot = useAppSelector(SelectSinglePlot); - const plotMarkers = useAppSelector(SelectPlotMarkers); - - const startTime = isOverlappingWaveform ? cycleLimits[0] : useAppSelector(MemoSelectStartTime); - const endTime = isOverlappingWaveform ? cycleLimits[1] : useAppSelector(MemoSelectEndTime); - - const analytics = useAppSelector(SelectAnalytics); - const overlappingEvents = useAppSelector(SelectEventList); - const useRelevantTime = useAppSelector(SelectUseOverlappingTime); - - const loading = useAppSelector(SelectLoading(props.dataKey)); - - const colors = useAppSelector(SelectColor); - const timeUnit = useAppSelector(SelectTimeUnit); - - const overlappingWaveTimeUnit = useAppSelector(SelectOverlappingWaveTimeUnit); - - const yLabels = useAppSelector(SelectYLabels(props.dataKey)); - const [yLblFontSize, setYLblFontSize] = React.useState(1); - - const mouseMode = useAppSelector(SelectMouseMode); - const zoomMode = useAppSelector(SelectZoomMode); - - const eventInfo = useAppSelector(SelectEventInfo); - const originalStartTime = new Date(eventInfo?.EventDate + "Z").getTime() - - const fftWindow = useAppSelector(SelectFFTWindow); - const showFFT = useAppSelector(SelectShowFFTWindow); - const fftCycles = useAppSelector(SelectCycles); - const { hover, setHover } = React.useContext(HoverContext); - - const [currentFFTWindow, setCurrentFFTWindow] = React.useState<[number, number]>(fftWindow); - const [oldFFTWindow, setOldFFTWindow] = React.useState<[number, number]>([0, 0]); - const [leftSelectCounter, setLeftSelectCounter] = React.useState(0); - - const points = useAppSelector(SelectDeltaHoverPoints(hover)); - - //Effect to update the Data - React.useEffect(() => { - if (lineData && lineData?.length > 0 && loading !== 'Loading') { - if (isCreated) - UpdateData(); - - createPlot(); - UpdateData(); - updateVisibility(); - setCreated(true); - } - - }, [lineData, loading]); - - - //Effect to adjust Axes Labels when Scale changes - React.useEffect(() => { - if (yScaleRef.current != undefined && xScaleRef.current != undefined) - updateSize(); - - }, [props.height, props.width]) - - React.useEffect(() => { - if (lineData && lineData?.length > 0) - updateVisibility(); - }, [enabledLine]) - - - //Effect to change location of tool tip - React.useEffect(() => { - if (xScaleRef.current) - setTooltipLocation(xScaleRef.current(hover?.[0])) - - updateHover(); - }, [hover]) - - //Effect to change location of tool tip - React.useEffect(() => { - if (xScaleRef.current) { - const newTime = points.length > 0 ? points[0].Time : null - if (newTime && selectedPointLocation !== xScaleRef.current(newTime)) - setSelectedPointLocation(xScaleRef.current(newTime)) - } - }, [points, startTime, endTime]) - - - // For performance Combine a bunch of Hooks that call updateLimits() since that is what re-renders the Lines - //Effect to adjust Axes when Units change - React.useEffect(() => { - if (yScaleRef.current === undefined || xScaleRef.current === undefined) - return; - - relevantUnits.forEach(unit => { - if (yScaleRef.current?.[unit] && yLimits?.[unit]) { - yScaleRef.current[unit].domain(yLimits[unit]); - } - }); - - if (enabledUnits?.length > 2) - xScaleRef.current.range([120, props.width - 110]) - else if (enabledUnits?.length > 3) - xScaleRef.current.range([120, props.width - 170]) - - if (yLimits) - updateLimits(); - - - }, [activeUnit, yLimits, startTime, endTime, isZoomed, timeUnit, lineData, useRelevantTime]) - - - React.useEffect(() => { - - if (leftSelectCounter == 0) - return; - if (leftSelectCounter == 1) - return; - const handle = setTimeout(() => { MouseLeft(); }, 500); - return () => { clearTimeout(handle) }; - }, [leftSelectCounter]) - - - React.useEffect(() => { //mouseDown Effect - if (!mouseDownInit) { - setMouseDownInit(true); - return; - } - - if (!mouseDown && mouseMode == 'zoom' && zoomMode == "x" && !isOverlappingWaveform) - dispatch(SetTimeLimit({ end: Math.max(pointMouse[0], hover[0]), start: Math.min(pointMouse[0], hover[0]) })) - if (!mouseDown && mouseMode == 'zoom' && zoomMode == "x" && !isOverlappingWaveform) - dispatch(SetCycleLimit({ end: Math.max(pointMouse[0], hover[0]), start: Math.min(pointMouse[0], hover[0]) })) - else if (!mouseDown && mouseMode == 'zoom' && zoomMode == "y") - dispatch(SetZoomedLimits({ limits: [Math.min(pointMouse[1], hover[1]), Math.max(pointMouse[1], hover[1])], key: props.dataKey })); - else if (!mouseDown && mouseMode == 'zoom' && zoomMode == "xy" && !isOverlappingWaveform) { - dispatch(SetZoomedLimits({ limits: [Math.min(pointMouse[1], hover[1]), Math.max(pointMouse[1], hover[1])], key: props.dataKey })); - } - else if (!fftMouseDown && mouseMode == 'fftMove' && pointMouse[0] < oldFFTWindow[1] && pointMouse[0] > oldFFTWindow[0]) { - const deltaT = pointMouse[0] - oldFFTWindow[0]; - const deltaData = oldFFTWindow[1] - oldFFTWindow[0]; - let Tstart = hover[0] - deltaT; - - Tstart = (Tstart < xScaleRef.current.domain()[0] ? xScaleRef.current.domain()[0] : Tstart) - Tstart = ((Tstart + deltaData) > xScaleRef.current.domain()[1] ? xScaleRef.current.domain()[1] - deltaData : Tstart); - dispatch(UpdateAnalytic({ settings: { ...analytics, FFTStartTime: Tstart, FFTCycles: fftCycles }, key: { DataType: "FFT", EventId: evtID } })); - } - }, [mouseDown, fftMouseDown]) - - - React.useEffect(() => { - updateColors(); - }, [colors]) - - - React.useEffect(() => { - updateFFTWindow(); - }, [fftWindow, showFFT, currentFFTWindow]) - - React.useEffect(() => { - if (xScaleRef.current) - updateDurationWindow(); - }, [plotMarkers, startTime, endTime, props.width, props.height, timeUnit]) - - React.useEffect(() => { - if (xScaleRef.current) - setCurrentFFTWindow([(xScaleRef.current(fftWindow[0])), (xScaleRef.current(fftWindow[1]))]); - }, [fftWindow]) - - - //This Clears the Plot if loading is activated - React.useEffect(() => { - d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId + ">svg").select("g.root").remove() - - if (loading == 'Loading') { - setCreated(false); - return; - } - - if (lineData?.length == 0) { - setCreated(false); - return; - } - - createPlot(); - UpdateData(); - updateVisibility(); - - }, [props.dataKey, options]); - - React.useEffect(() => { - const container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - if (container == null || container.select(".yAxisLabel") == null) - return; - relevantUnits.forEach(unit => { - container.select(`.yAxisLabelLeft[type='${unit}']`).style('font-size', yLblFontSize.toString() + 'rem'); - container.select(`.yAxisLabelLeft[type='${unit}']`).text(yLabels[unit]) - - container.select(`.yAxisLabelRight[type='${unit}']`).style('font-size', yLblFontSize.toString() + 'rem'); - container.select(`.yAxisLabelRight[type='${unit}']`).text(yLabels[unit]) - }) - - }, [yLabels, yLblFontSize]); - - React.useEffect(() => { - const container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - if (container == null || container.select(".yAxisLabel") == null) - return; - - let fs = 1; - let l = GetTextWidth('', '1rem', yLabels?.[primaryAxis]); - let r = GetTextWidth('', '1rem', yLabels?.[primaryAxis] ? yLabels?.[primaryAxis] : ""); - - while (((l > props.height - 60) || (r > props.height - 60)) && fs > 0.2) { - fs = fs - 0.05; - l = GetTextWidth('', fs.toString() + 'rem', yLabels?.[primaryAxis]); - r = GetTextWidth('', fs.toString() + 'rem', yLabels?.[primaryAxis] ? yLabels?.[primaryAxis] : ""); - } - if (fs != yLblFontSize) - setYLblFontSize(fs) - - }, [props.height, yLabels]) - - function createLineGen(unit: OpenSee.Unit = null, base = null) { - let factor = 1.0 - - // Calculate factor if unit and base are provided - if (unit && base && activeUnit?.[unit]) { - factor = activeUnit?.[unit].factor; - if (factor === undefined) //p.u case - factor = 1.0 / base - } - - return d3.line() - .x(d => { - return xScaleRef.current ? xScaleRef.current(d[0]) : 0 - }) - .y(d => yScaleRef?.current[unit] ? yScaleRef?.current[unit](d[1] * factor) : 0) - .defined(d => { - let tx = !isNaN(parseFloat(xScaleRef.current ? xScaleRef.current(d[0])?.toString() : '0')); - let ty = !isNaN(parseFloat(yScaleRef?.current[unit] ? yScaleRef.current[unit](d[1] * factor)?.toString() : '0')); - tx = tx && isFinite(parseFloat(xScaleRef.current ? xScaleRef.current(d[0])?.toString() : '0')); - ty = ty && isFinite(parseFloat(yScaleRef?.current[unit] ? yScaleRef.current[unit](d[1] * factor)?.toString() : '0')); - return tx && ty; - }); - } - - - // This Function needs to be called whenever Data is Added - function UpdateData() { - // Set x scale range based on the number of enabled units - - if (enabledUnits?.length > 2) - xScaleRef.current.range([120, props.width - 110]); - if (enabledUnits?.length > 3) - xScaleRef.current.range([120, props.width - 170]); - - - const container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - const lines = container.select(".DataContainer").selectAll(".Line").data(lineData); - - lines.enter().append("path").classed(`Line`, true) - .attr("type", d => `${d.Unit}`) - .attr("stroke", d => (Object.keys(colors).indexOf(d.Color) > -1 ? colors[d.Color] : colors.random)) - .attr("stroke-dasharray", d => singlePlot && evtID !== d.EventID ? 5 : 0) - .attr("d", d => { - const lineGen = createLineGen(d.Unit) - if (d.SmoothDataPoints.length > 0) - return lineGen.curve(d3.curveNatural)(d.SmoothDataPoints); - return lineGen(d.DataPoints); - }) - - lines.exit().remove(); - - const points = container.select(".DataContainer").selectAll(".Markers") - .data(lineData) - .enter() - .append("g") - .attr("fill", d => (Object.keys(colors).indexOf(d.Color) > -1 ? colors[d.Color] : colors.random)) - .classed("Markers", true) - .selectAll("circle") - .data(d => d.DataMarker.map(v => ({ - x: v[0], y: v[1], unit: d.Unit as string, base: d.BaseValue - }) as IMarker) - ); - - - points.enter() - .append("circle") - .classed("Circle", true) - .attr("cx", d => isNaN(xScaleRef.current(d[0])) ? null : xScaleRef.current(d.x)) - .attr("cy", d => isNaN(yScaleRef.current[d.unit](d[1])) ? null : yScaleRef.current[d.unit](d.y)) - .attr("r", 10); - - - points.exit().remove(); - - updateLimits(); - updateDurationWindow(); - } - - - // This Function should be called anytime the Scale changes as it will adjust the Axis, Path and Points - function updateLimits() { - const container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - const svg = container.select(".DataContainer"); - - svg.selectAll(".Line").attr("d", function (d: OpenSee.iD3DataSeries) { - const scopedLineGen = createLineGen(d.Unit, d.BaseValue); - if (d.SmoothDataPoints.length > 0) - return scopedLineGen.curve(d3.curveNatural)(d.SmoothDataPoints); - return scopedLineGen(d.DataPoints); - }); - - svg.selectAll("circle") - .attr("cx", function (d: IMarker) { - return isNaN(xScaleRef.current(d.x)) ? null : xScaleRef.current(d.x); - }) - .attr("cy", function (d: IMarker) { - let factor = 1.0; - if (activeUnit?.[d.unit] != undefined) - factor = activeUnit?.[d.unit].factor === undefined ? (1.0 / d.base) : factor; - - return isNaN(yScaleRef.current[d.unit](d.y)) ? null : yScaleRef.current[d.unit](d.y * factor); - }); - - updateYAxises() - updateLabels(); - - //Format Time Axis with current xScale - container.selectAll(".xAxis").transition().call(d3.axisBottom(xScaleRef.current).tickFormat(d => formatTimeTick(d as number)) as any); - - if (xScaleRef.current != null && showFFT) { - setCurrentFFTWindow([(xScaleRef.current(fftWindow[0])), (xScaleRef.current(fftWindow[1]))]); - } - - } - - function createPlot() { - d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId + ">svg").select("g.root").remove() - - const svg = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId).select("svg") - .append("g").classed("root", true) - .attr("transform", "translate(10,0)"); - - // Everything should start at 40, 20 except the div for overlay... - //Update x/y scales - if (yLimits) { - Object.keys(yLimits).forEach(unit => { - if (yLimits?.[unit]) - yScaleRef.current[unit] = d3.scaleLinear().domain(yLimits?.[unit]).range([props.height - 40, 20]); - else - yScaleRef.current[unit] = d3.scaleLinear().domain([0, 1]).range([props.height - 40, 20]); - }) - } - - xScaleRef.current = d3.scaleLinear().domain([startTime, endTime]).range([60, props.width - 110]) - - //Create xAxis - svg.append("g").classed("xAxis", true).attr("transform", "translate(0," + (props.height - 40) + ")").call(d3.axisBottom(xScaleRef.current).tickFormat((d) => formatTimeTick(d as number))); - - let isAxisLeft = true; - - //Create yAxises that have enabled Units - enabledUnits.forEach(unit => { - const axisTransform = isAxisLeft ? "translate(60,0)" : `translate(${props.width - 110},0)`; - - svg.append("g") - .classed(`yAxis`, true) - .attr("type", `${unit}`) - .attr("transform", axisTransform) - .call(isAxisLeft ? d3.axisLeft(yScaleRef.current[unit]).tickFormat(d => formatValueTick(d as number, unit)) : d3.axisRight(yScaleRef.current[unit]).tickFormat(d => formatValueTick(d as number, unit))) - .style("opacity", 1) - - // Create axis label - const labelYPos = isAxisLeft ? 2 : props.width - 70; - - svg.append("text") - .classed(isAxisLeft ? `yAxisLabelLeft` : `yAxisLabelRight`, true) - .attr("type", `${unit}`) - .attr("x", - (props.height / 2 - 20)) - .attr("y", labelYPos) - .attr("dy", "1em") - .attr("transform", "rotate(-90)") - .style("text-anchor", "middle") - .style("opacity", 1) - .text(yLabels[unit]); - - isAxisLeft = !isAxisLeft; - }); - - //Create Axis Labels - svg.append("text").classed("xAxisLabel", true) - .attr("transform", "translate(" + ((props.width - 210) / 2 + 60) + " ," + (props.height - 5) + ")") - .style("text-anchor", "middle") - .text("Time"); - - // Create Plot Title - svg.append("text").classed("plotTitle", true) - .attr("transform", "translate(" + ((props.width - 210) / 2 + 60) + ", 20)") - .style("text-anchor", "middle") - .style("font-weight", "bold") - .text(GetDisplayLabel(props.dataKey.DataType)); - - setTooltipLocation(10); - - //Add Clip Path - svg.append("defs").append("svg:clipPath") - .attr("id", "clipData-" + props.dataKey.DataType + "-" + props.dataKey.EventId) - .append("svg:rect").classed("clip", true) - .attr("width", props.width - 170) - .attr("height", props.height - 60) - .attr("x", 60) - .attr("y", 20); - - //Add Window to indicate Zooming - svg.append("rect").classed("zoomWindow", true) - .attr("stroke", "#000") - .attr("x", 60).attr("width", 0) - .attr("y", 20).attr("height", props.height - 60) - .attr("fill", "black") - .style("opacity", 0); - - //Add Window to indicate Inception and Duration of event - svg.append("rect").classed("DurationWindow", true) - .attr("clip-path", "url(#clipData-" + props.dataKey.DataType + "-" + props.dataKey.EventId + ")") - .attr("stroke", "#d3d3d3") - .attr("x", xScaleRef.current(eventInfo?.Inception)) - .attr("width", eventInfo?.DurationEndTime - eventInfo?.Inception) - .style("opacity", (plotMarkers ? 0.25 : 0)) - .attr("y", 20).attr("height", props.height - 60) - .attr("fill", "black") - - //Add Empty group for Data Points - svg.append("g").classed("DataContainer", true) - .attr("clip-path", "url(#clipData-" + props.dataKey.DataType + "-" + props.dataKey.EventId + ")") - .style("transition", 'd 0.5s') - .attr("fill", "none") - .attr("stroke-width", 0.0); - - //Event overlay - needs to be treated seperately - svg.append("svg:rect").classed("Overlay", true) - .attr("width", props.width - 110) - .attr("height", '100%') - .attr("x", 20) - .attr("y", 0) - .style("opacity", 0) - .on('mousemove', (evt) => MouseMove(evt)) - .on('mouseout', () => MouseOut()) - .on('mousedown', (evt) => MouseDown(evt)) - .on('mouseup', () => MouseUp()) - .on('mouseenter', () => { setLeftSelectCounter(1) }) - .call(wheelZoom) - .on('wheel', (evt) => evt.preventDefault()); - - - //Window to indicate FFT -- this needs to be placed after the Event Overlay so it can capture mouseEvents - if (props.dataKey.DataType == 'Voltage' || props.dataKey.DataType == 'Current') - svg.append("rect").classed("fftWindow", true) - .attr("clip-path", "url(#clipData-" + props.dataKey.DataType + "-" + props.dataKey.EventId + ")") - .attr("stroke", "#000") - .style("z-index", 9999) - .attr("x", xScaleRef.current(fftWindow[0])) - .attr("width", currentFFTWindow[1] - currentFFTWindow[0]) - .style("opacity", (showFFT ? 0.5 : 0)) - .style('cursor', (mouseMode === 'fftMove' && showFFT ? 'grab' : 'default')) - .attr("y", 20).attr("height", props.height - 60) - .attr("fill", "black") - .on('mousemove', (evt) => MouseMove(evt)) - .on('mousedown', (evt) => FFTMouseDown(evt)) - .on('mouseup', () => setFFTMouseDown(false)) - - } - - - function formatTimeTick(d: number) { - let TS = moment(d); - let h = 100; - - if (xScaleRef.current != undefined) - h = xScaleRef.current.domain()[1] - xScaleRef.current.domain()[0] - - if (isOverlappingWaveform) { - if (defaultSettings.OverlappingWaveTimeUnit.options[overlappingWaveTimeUnit].short === "ms") { - if (h < 2) - return d.toFixed(3) - if (h < 5) - return d.toFixed(2) - else - return d.toFixed(1) - } else if (defaultSettings.OverlappingWaveTimeUnit.options[overlappingWaveTimeUnit].short === "cycles") { - const cyc = d * 60.0 / 1000.0; - h = h * 60.0 / 1000.0; - if (h < 2) - return cyc.toFixed(3) - if (h < 5) - return cyc.toFixed(2) - else - return cyc.toFixed(1) - } - - } - else if (timeUnit.options[timeUnit.current].short == 'auto') { - if (h < 100) - return TS.format("SSS.S") - else if (h < 1000) - return TS.format("ss.SS") - else - return TS.format("ss.S") - } - else if (timeUnit.options[timeUnit.current].short == 's') { - if (h < 100) - return TS.format("ss.SSS") - else if (h < 1000) - return TS.format("ss.SS") - else - return TS.format("ss.S") - } - else if (timeUnit.options[timeUnit.current].short == 'ms') - if (h < 100) - return TS.format("SSS.S") - else - return TS.format("SSS") - - else if (timeUnit.options[timeUnit.current].short == 'min') - return TS.format("mm:ss") - - else if (timeUnit.options[timeUnit.current].short == 'ms since record') { - let ms = d - originalStartTime; - - if (useRelevantTime && !isOriginalEvt) { - const evt = overlappingEvents.find(evt => evt.EventID === props.dataKey.EventId) - ms = d - evt?.StartTime - } - - if (h < 2) - return ms.toFixed(3) - if (h < 5) - return ms.toFixed(2) - else - return ms.toFixed(1) - } - - else if (timeUnit.options[timeUnit.current].short == 'ms since inception') { - let ms = d - (new Date(eventInfo?.InceptionDate + "Z").getTime()); - - if (useRelevantTime && !isOriginalEvt) { - const evt = overlappingEvents.find(evt => evt.EventID === props.dataKey.EventId) - ms = d - evt?.Inception - } - - if (h < 2) - return ms.toFixed(3) - if (h < 5) - return ms.toFixed(2) - else - return ms.toFixed(1) - } - - else if (timeUnit.options[timeUnit.current].short == 'cycles since record') { - let cyc = (d - startTime) * 60.0 / 1000.0; - - h = h * 60.0 / 1000.0; - if (h < 2) - return cyc.toFixed(3) - if (h < 5) - return cyc.toFixed(2) - else - return cyc.toFixed(1) - } - else if (timeUnit.options[timeUnit.current].short == 'cycles since inception') { - let cyc = (d - startTime) * 60.0 / 1000.0; - - h = h * 60.0 / 1000.0; - if (h < 2) - return cyc.toFixed(3) - if (h < 5) - return cyc.toFixed(2) - else - return cyc.toFixed(1) - } - } - - function formatValueTick(d: number, unit: OpenSee.Unit) { - let h = 1; - - if (yScaleRef.current) - h = yScaleRef.current[unit].domain()[1] - yScaleRef.current[unit].domain()[0] - - if (Math.abs(d) >= 100000) { - return d.toString().slice(0, 4) + '...'; - } - - if (h > 100) - return d.toFixed(0) - - if (h > 10) - return d.toFixed(1) - else - return d.toFixed(2) - - } - - function MouseMove(evt) { - - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - let x0 = d3.pointer(evt, container.select(".Overlay").node())[0]; - let y0 = d3.pointer(evt, container.select(".Overlay").node())[1]; - - if (x0 < 60) - x0 = 60; - if (x0 > (props.width - 140)) - x0 = props.width - 140; - - if (y0 < 20) - y0 = 20; - if (y0 > (props.height - 40)) - y0 = props.height - 40; - - let t0 = xScaleRef.current.invert(x0) - let d0 = yScaleRef.current[primaryAxis].invert(y0); - setHover([t0, d0]) - } - - function MouseDown(evt) { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - let x0 = d3.pointer(evt, container.select(".Overlay").node())[0]; - let y0 = d3.pointer(evt, container.select(".Overlay").node())[1]; - - let t0 = xScaleRef.current.invert(x0); - let d0 = yScaleRef.current[primaryAxis].invert(y0); - - setMouseDown(true); - setPointMouse([t0, d0]); - - if (isOverlappingWaveform) - return; - - if (x0 > 60 && x0 < props.width - 140 && mouseMode === 'select') - dispatch(SetSelectPoint({ time: t0, key: props.dataKey })); - - setOldFFTWindow(() => { - return fftWindow - }); - - } - - function FFTMouseDown(evt) { - setFFTMouseDown(true); - - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - let x0 = d3.pointer(evt, container.select(".Overlay").node())[0]; - let y0 = d3.pointer(evt, container.select(".Overlay").node())[1]; - - let t0 = xScaleRef.current.invert(x0); - let d0 = yScaleRef.current[primaryAxis].invert(y0); - - setPointMouse([t0, d0]); - - setOldFFTWindow(() => { - return fftWindow - }); - - } - - function MouseUp() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - setMouseDown(false); - container.select(".zoomWindow").style("opacity", 0) - } - - // This function needs to be called if hover is updated - function updateHover() { - - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - if (xScaleRef.current == undefined || yScaleRef.current == undefined) - return; - - //container.select(".toolTip").attr("x1", xScaleRef.current(hover[0])) - // .attr("x2", xScaleRef.current(hover[0])); - - if (mouseMode == 'zoom' && mouseDown) { - if (zoomMode == "x") - container.select(".zoomWindow").style("opacity", 0.5) - .attr("x", xScaleRef.current(Math.min(hover[0], pointMouse[0]))) - .attr("width", Math.abs(xScaleRef.current(hover[0]) - xScaleRef.current(pointMouse[0]))) - .attr("height", props.height - 60) - .attr("y", 20) - else if (zoomMode == "y") - container.select(".zoomWindow").style("opacity", 0.5) - .attr("x", xScaleRef.current(startTime)) - .attr("width", xScaleRef.current(endTime) - xScaleRef.current(startTime)) - .attr("height", Math.abs(yScaleRef.current[primaryAxis](pointMouse[1]) - yScaleRef.current[primaryAxis](hover[1]))) - .attr("y", Math.min(yScaleRef.current[primaryAxis](pointMouse[1]), yScaleRef.current[primaryAxis](hover[1]))) - else if (zoomMode == "xy") - container.select(".zoomWindow").style("opacity", 0.5) - .attr("x", xScaleRef.current(Math.min(hover[0], pointMouse[0]))) - .attr("width", Math.abs(xScaleRef.current(hover[0]) - xScaleRef.current(pointMouse[0]))) - .attr("height", Math.abs(yScaleRef.current[primaryAxis](pointMouse[1]) - yScaleRef.current[primaryAxis](hover[1]))) - .attr("y", Math.min(yScaleRef.current[primaryAxis](pointMouse[1]), yScaleRef.current[primaryAxis](hover[1]))) - } - - let deltaT = hover[0] - pointMouse[0]; - let deltaData = hover[1] - pointMouse[1]; - - if (mouseMode === 'pan' && mouseDown && (zoomMode === "x" || zoomMode === "xy")) { - if (!isOverlappingWaveform) { - dispatch(SetTimeLimit({ start: (startTime - deltaT), end: (endTime - deltaT) })); - } else if (isOverlappingWaveform) { - dispatch(SetCycleLimit({ start: (startTime - deltaT), end: (endTime - deltaT) })); - } - } - - if (mouseMode === 'pan' && mouseDown && (zoomMode === "y" || zoomMode === "xy")) { - dispatch(SetZoomedLimits({ limits: [(yLimits[primaryAxis][0] - deltaData), (yLimits[primaryAxis][1] - deltaData)], key: props.dataKey })); - } - - - if (mouseMode == 'fftMove' && fftMouseDown && pointMouse[0] < oldFFTWindow[1] && pointMouse[0] > oldFFTWindow[0]) { - setCurrentFFTWindow([xScaleRef.current(oldFFTWindow[0] + hover[0] - pointMouse[0]), xScaleRef.current(oldFFTWindow[1] + hover[0] - pointMouse[0])]) - } - - } - - function updateFFTWindow() { - if (props.dataKey.DataType != 'Voltage' && props.dataKey.DataType != 'Current') - return; - - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - if (xScaleRef.current == undefined || yScaleRef.current == undefined) - return; - - container.select(".fftWindow") - .attr("x", currentFFTWindow[0]) - .attr("width", currentFFTWindow[1] - currentFFTWindow[0]) - .style("opacity", (showFFT ? 0.5 : 0)) - .style('cursor', (mouseMode === 'fftMove' && showFFT ? 'grab' : 'default')) - - } - - function updateYAxises() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - //Flag to alternate axis placement - if (container === undefined) - return - - let isAxisLeft = true; //this can just be exchanged for a % 2 since we have a counter now. - let currentAxis = 0; - - //Update yAxises - enabledUnits?.forEach(unit => { - let axisType = `[type='${unit}']`; - let firstLeftAxisType = `[type='${enabledUnits[0]}']` - let firstRightAxisType = `[type='${enabledUnits[1]}']` - - if (isAxisLeft) { - if (currentAxis > 1) { - container.selectAll(`.yAxis${firstLeftAxisType}`).attr("transform", "translate(120, 0)") - container.selectAll(`.yAxisLabelLeft${firstLeftAxisType}`).attr("y", "62") - } - container.selectAll(`.yAxis${axisType}`).transition().call(d3.axisLeft(yScaleRef.current[unit]).tickFormat(d => formatValueTick(d as number, unit)) as any); - } - else { - if (currentAxis > 2) { - container.selectAll(`.yAxis${firstRightAxisType}`).attr("transform", `translate(${props.width - 170},0)`) - container.selectAll(`.yAxisLabelRight${firstRightAxisType}`).attr("y", props.width - 135) - } - container.selectAll(`.yAxis`).selectAll(`[type='${unit}']`).transition().call(d3.axisRight(yScaleRef.current[unit]).tickFormat(d => formatValueTick(d as number, unit)) as any); - } - - isAxisLeft = !isAxisLeft; - currentAxis++; - - - }); - - - if (enabledUnits.length < 3) - return - - let clipPath = container.select(`#clipData-${props.dataKey.DataType}-${props.dataKey.EventId} > rect`) - let evtOverlay = container.select(`rect.Overlay`) - - if (enabledUnits.length === 3) { - clipPath.attr("x", 120).attr("width", props.width - 270) - evtOverlay.attr("x", 120).attr("width", props.width - 270) - } - else if (enabledUnits.length === 4) { - clipPath.attr("x", 120).attr("width", props.width - 210 - 120) - evtOverlay.attr("x", 120).attr("width", props.width - 210 - 120) - } - - } - - function updateDurationWindow() { - if (xScaleRef.current === undefined) - return; - - setInceptionLocation(xScaleRef.current(eventInfo?.Inception)) - setDurationLocation(xScaleRef.current(eventInfo?.DurationEndTime)) - - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - let width = 1 - let x = 1 - - width = xScaleRef.current(eventInfo?.DurationEndTime) - xScaleRef.current(eventInfo?.Inception) - x = xScaleRef.current(eventInfo?.Inception) - - - container.select(".DurationWindow") - .attr("x", x) - .attr("width", width) - .style("opacity", (plotMarkers ? 0.25 : 0)) - } - - const wheelZoom = d3.zoom() //probably could include panning in here... - .filter(event => { - return event.type === 'wheel'; - }) - .on("zoom", (event) => { - //need to scale here whenever since inception is enabled and overlapping stuff.. - let newTime = event.transform.rescaleX(xScaleRef.current).domain(); - let newYLimits = event.transform.rescaleX(yScaleRef.current[primaryAxis]).domain(); - - if (mouseMode == 'zoom' && zoomMode == "x" && !isOverlappingWaveform) - dispatch(SetTimeLimit({ start: newTime[0], end: newTime[1] })) - - if (mouseMode == 'zoom' && zoomMode == "y" && !isOverlappingWaveform) - dispatch(SetZoomedLimits({ limits: newYLimits, key: props.dataKey })) - - if (mouseMode == 'zoom' && zoomMode == "xy" && !isOverlappingWaveform) { - dispatch(SetTimeLimit({ start: newTime[0], end: newTime[1] })) - dispatch(SetZoomedLimits({ limits: newYLimits, key: props.dataKey })) - } - - }); - - function MouseOut() { - setLeftSelectCounter(() => -1); - } - - // Mouse Left only get's called if we left for a minimum of time - function MouseLeft() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - container.select(".zoomWindow").style("opacity", 0); - setMouseDown(false); - } - - //This function needs to be called whenever (a) Unit Changes (b) Data Changes (c) Data Visibility changes (d) Limits change (due to auto Units)s - function updateLabels() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - function GetTLabel() { - let h = 100; - if (xScaleRef.current != undefined) - h = xScaleRef.current.domain()[1] - xScaleRef.current.domain()[0] - - - if ((timeUnit as OpenSee.IUnitSetting).options[timeUnit.current].short != 'auto' && !isOverlappingWaveform) - return (timeUnit as OpenSee.IUnitSetting).options[timeUnit.current].short; - - if (isOverlappingWaveform) { - if (h < 100) - return "ms" - else - return "s" - } - - if ((timeUnit as OpenSee.IUnitSetting).options[timeUnit.current].short == 'ms since event') - return "ms"; - if ((timeUnit as OpenSee.IUnitSetting).options[timeUnit.current].short == 'cycles') - return "cycle" - if (h < 100) - return "ms" - else - return "s" - } - - container.select(".xAxisLabel").text("Time (" + GetTLabel() + ")") - } - - //This Function needs to be called whenever (a) Color Setting changes occur - function updateColors() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - function GetColor(col: OpenSee.Color) { - return colors[col as string] - } - - container.select(".DataContainer").selectAll(".Line").attr("stroke", (d: OpenSee.iD3DataSeries) => GetColor(d.Color)); - container.select(".DataContainer").selectAll(".Markers").attr("fill", (d: OpenSee.iD3DataSeries) => GetColor(d.Color)); - } - - //This Function needs to be called whenever a item is selected or deselected in the Legend - function updateVisibility() { - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - // Update line visibility for each unit - container.selectAll(`.Line`).data(lineData) - .classed("active", d => d.Enabled) - .attr("stroke-width", d => d.Enabled ? 2.5 : 0); - - // Update markers for primary lines - container.selectAll(`.Markers`).data(lineData) - .classed("active", d => d.Enabled) - .attr("opacity", d => d.Enabled ? 1.0 : 0); - - - // Update axis visibility based on whether the unit is enabled - let isAxisLeft = true; - - relevantUnits.forEach(unit => { - let enabledUnit = enabledUnits?.includes(unit); - let axisType = `[type='${unit}']`; - if (enabledUnit) { - container.selectAll(`.yAxis${axisType}`).style("opacity", 1); - container.selectAll(`.yAxisLabel${axisType}`).style("opacity", 1); - } else { - container.selectAll(`.yAxis${axisType}`).remove(); - container.selectAll(`.yAxisLabel${axisType}`).remove(); - } - - isAxisLeft = !isAxisLeft; - }) - - } - - // This Function needs to be called whenever height or width change - function updateSize() { - - let container = d3.select("#graphWindow-" + props.dataKey.DataType + "-" + props.dataKey.EventId); - - container.select(".xAxisLabel").attr("transform", "translate(" + ((props.width - 210) / 2 + 60) + " ," + (props.height - 5) + ")") - container.select(".plotTitle").attr("transform", "translate(" + ((props.width - 210) / 2 + 60) + ",20)").style("font-weight", "bold") - - //this is gonna have to change for the addition of more than 2 axises - container.select(".yAxisLabelLeft").attr("x", - (props.height / 2 - 20)) - container.select(".yAxisLabelRight").attr("y", props.width - 120).attr("x", - (props.height / 2 - 20)) - - let isAxisLeft = true; - relevantUnits.forEach(unit => { - //Update yScale - yScaleRef.current[unit].range([props.height - 40, 20]); - - let axisType = `[type='${unit}']`; - let axisTransform = isAxisLeft ? "translate(60,0)" : `translate(${props.width - 110},0)`; - - if (isAxisLeft) - container.selectAll(`.yAxis${axisType}`).attr("transform", axisTransform); - else - container.selectAll(`.yAxis${axisType}`).attr("transform", axisTransform); - - isAxisLeft = !isAxisLeft; - }) - - - // Set x scale range based on the number of enabled units - xScaleRef.current.range([60, props.width - 110]); - - if (enabledUnits?.length > 2) { - xScaleRef.current.range([120, props.width - 110]); - } else if (enabledUnits?.length > 3) { - xScaleRef.current.range([120, props.width - 170]); - } - - container.select(".xAxis").attr("transform", "translate(0," + (props.height - 40) + ")") - - container.select(".clip").attr("width", props.width - 110).attr("height", props.height - 60) - container.select(".fftwindow").attr("height", props.height - 60); - container.select(".Overlay").attr("width", props.width - 110) - updateLimits(); - } - - // Helper Function - function GetTextWidth(font: string, fontSize: string, word: string): number { - - const text = document.createElement("span"); - document.body.appendChild(text); - - text.style.font = font; - text.style.fontSize = fontSize; - text.style.height = 'auto'; - text.style.width = 'auto'; - text.style.position = 'absolute'; - text.style.whiteSpace = 'no-wrap'; - text.innerHTML = word; - - const width = Math.ceil(text.clientWidth); - document.body.removeChild(text); - return width; - } - - return ( -
    - 0} hasTrace={enabledLine?.some(i => i)} - selectedPointLocation={selectedPointLocation} showToolTip={props.showToolTip} inceptionLocation={inceptionLocation} durationLocation={durationLocation} plotMarkers={plotMarkers} /> - {loading === 'Loading' || lineData?.length == 0 ? null : } -
    - ); -} - - -const Container = React.memo((props: { - height: number, dataKey: OpenSee.IGraphProps, loading: OpenSee.LoadingState, hover: number, hasData: boolean, - hasTrace: boolean, selectedPointLocation: number, showToolTip: boolean, inceptionLocation: number, durationLocation: number, plotMarkers: boolean -}) => { - const showSVG = props.loading != 'Loading' && props.hasData; - return ( -
    - {props.loading === 'Loading' ? : null} - {props.loading != 'Loading' && props.loading != 'Error' && !props.hasData ? : null} - {props.loading === 'Error' ? : null} - - - { /*PolyLine for the mouse position*/} - {props.loading !== 'Loading' && props.hasData ? : null} - { /*PolyLine for the position of Selected Point*/} - {props.showToolTip && props.selectedPointLocation ? : null} - - { /*PolyLine for the inception of the event*/} - {props.loading !== 'Loading' && props.hasData && props.plotMarkers ? : null} - { /*PolyLine for the end of the duration of the event*/} - {props.loading !== 'Loading' && props.hasData && props.plotMarkers ? : null} - - {props.loading != 'Loading' && props.hasData && !props.hasTrace ? - Select a Trace in the Legend to Display. : null} - -
    - ) -}) - -const PolyLine = (props: { height: number, left: number, style: React.CSSProperties, class: string }) => { - return ( - - - - ) -} - -export default LineChart; - diff --git a/src/OpenSEE/Scripts/TSX/Graphs/Utilities.tsx b/src/OpenSEE/Scripts/TSX/Graphs/Utilities.tsx deleted file mode 100644 index 0c966705..00000000 --- a/src/OpenSEE/Scripts/TSX/Graphs/Utilities.tsx +++ /dev/null @@ -1,75 +0,0 @@ -//****************************************************************************************************** -// Utilities.tsx - Gbtc -// -// Copyright © 2021, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 02/23/2021 - C. Lackner -// Generated original version of source code -// -//****************************************************************************************************** - -import { OpenSee } from "../global" -import { defaultSettings } from "../defaults" - -export function GetDisplayLabel(type: OpenSee.graphType): string { - switch (type) { - case ('FirstDerivative'): - return "First Derivative" - case ('HighPassFilter'): - return "High Pass Filter" - case ('LowPassFilter'): - return "Low Pass Filter" - case ('ClippedWaveforms'): - return "Fixed Waveforms" - case ('OverlappingWave'): - return "Overlapping Waveform" - case ('MissingVoltage'): - return "Missing Voltage" - case ('Rectifier'): - return 'Rectifier Output'; - case ('RapidVoltage'): - return "Rapid Voltage Change" - case ('RemoveCurrent'): - return "Remove Current" - case ('Harmonic'): - return "Specified Harmonic" - case ('SymetricComp'): - return "Symmetrical Components" - case ('FaultDistance'): - return "Fault Distance" - case ('Restrike'): - return "Breaker Restrike" - default: - return (type as string) - } -} - -export function sortGraph(item1: OpenSee.IGraphProps, item2: OpenSee.IGraphProps): number { - if (item1.DataType == item2.DataType) - return 0 - - let index1 = defaultSettings.PlotOrder.findIndex((v) => v == item1.DataType); - let index2 = defaultSettings.PlotOrder.findIndex((v) => v == item2.DataType); - - if (index1 != -1 && index2 != -1) - return (index1 > index2 ? 1 : -1); - if (index1 != -1) - return -1; - if (index2 != -1) - return 1; - - return (item1 > item2 ? 1 : -1); -} \ No newline at end of file diff --git a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/AccumulatedPoints.tsx b/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/AccumulatedPoints.tsx deleted file mode 100644 index 25f2908f..00000000 --- a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/AccumulatedPoints.tsx +++ /dev/null @@ -1,128 +0,0 @@ -//****************************************************************************************************** -// AccumulatedPoints.tsx - Gbtc -// -// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 05/11/2018 - Billy Ernest -// Generated original version of source code. -// -// 01/24/2024 - Preston Crawford -// Fix Remove point button / refactor table layout -// -//****************************************************************************************************** -import * as React from 'react'; -import { SelectSelectedPoints, SelectStartTime, RemoveSelectPoints, ClearSelectPoints } from '../store/dataSlice'; -import { SelectColor } from '../store/settingSlice' -import { useAppDispatch, useAppSelector } from '../hooks'; - - -const PointWidget = () => { - const points = useAppSelector(SelectSelectedPoints); - const startTime = useAppSelector(SelectStartTime); - const dispatch = useAppDispatch(); - const colors = useAppSelector(SelectColor); - const [selectedIndex, setSelectedIndex] = React.useState(-1); - - const flexRef = React.useRef(null); - const firstCellRef = React.useRef(null); - const secondCellRef = React.useRef(null); - const [flexSize, setFlexSize] = React.useState<{ width: number, height: number }>({ width: 0, height: 0 }); - const [leftPosition, setLeftPosition] = React.useState({ secondCell: 0, thirdCell: 0 }); - - - React.useLayoutEffect(() => { - const firstCellWidth = firstCellRef.current ? firstCellRef.current.offsetWidth : 0; - const secondCellWidth = secondCellRef.current ? secondCellRef.current.offsetWidth : 0; - - setLeftPosition({ secondCell: firstCellWidth, thirdCell: firstCellWidth + secondCellWidth }); - }, [points]); - - React.useLayoutEffect(() => { - if (flexRef.current) - setFlexSize({ width: flexRef.current.offsetWidth, height: flexRef.current.offsetHeight }); - }, []); - - return ( - <> -
    -
    - - - - - - - {points[0]?.Value?.map((p, i) => ( - - ))} - - - - {points.map((point, pointIndex) => ( - - - - - {point.Value.map((p, i) => ( - - ))} - - - ))} - -
    -     - - Time - - Value -
    - Delta -
    - - {(p[0] - startTime).toFixed(7)} sec
    {((p[0] - startTime) * 60.0).toFixed(2)} cycles -
    -
    -     - - {point.Group} - - Value -
    - Delta -
    setSelectedIndex(i)} style={{ backgroundColor: (selectedIndex == i ? 'yellow' : null), textAlign: 'center', verticalAlign: 'middle' }}> - - {(p[1] * (point.Unit?.factor === undefined ? 1.0 / point.BaseValue : point.Unit?.factor)).toFixed(2)} {point?.Unit?.short} - -
    - - {i === 0 ? 'N/A' : - ((point.Value[i - 1][1] - p[1]) * (point.Unit?.factor === undefined ? 1.0 / point.BaseValue : point.Unit?.factor)).toFixed(4)} {point?.Unit?.short} - -
    -
    -
    - { if (selectedIndex !== -1) dispatch(RemoveSelectPoints(selectedIndex)); setSelectedIndex(-1) }} /> - dispatch(RemoveSelectPoints(points[0].Value.length - 1))} /> - { dispatch(ClearSelectPoints()); setSelectedIndex(-1) }} /> -
    -
    - - ); - -} - -export default PointWidget; diff --git a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/EventInfo.tsx b/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/EventInfo.tsx deleted file mode 100644 index f800b1b6..00000000 --- a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/EventInfo.tsx +++ /dev/null @@ -1,104 +0,0 @@ -//****************************************************************************************************** -// EventInfo.tsx - Gbtc -// -// Copyright 2018, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 12/27/2023 - Preston Crawford -// Generated original version of source code. -//****************************************************************************************************** - -import React from 'react'; -import { useAppSelector } from '../hooks'; -import { SelectEventInfo } from '../store/eventInfoSlice' -import queryString from 'querystring'; -import moment from 'moment'; - -const eventDateFormat = "YYYY-MM-DD HH:mm:ss.fffffff"; -const dateFormat = "MM/DD/YYYY"; -const timeFormat = "HH:mm:ss.SSS"; -const EventInfo = () => { - const eventData = useAppSelector(SelectEventInfo) - const [pqBrowserURL, setPqBrowserURL] = React.useState('http://localhost:44368') - const [pqBrowserParams, setPQBrowserParams] = React.useState("") - - React.useEffect(() => { - const handle1 = getPQUrl(); - handle1.done((data) => setPqBrowserURL(data)); - }, []) - - function getPQUrl() { - return $.ajax({ - type: "GET", - url: `${homePath}api/OpenSEE/GetPQBrowser/`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: true, - async: true - }); - } - - React.useEffect(() => { - const time = moment.utc(eventData.EventDate, eventDateFormat).format(timeFormat) - const date = moment.utc(eventData.EventDate, eventDateFormat).format(dateFormat) - const queryParams = { - eventid: eventID, - time: time, - date: date, - windowSize: 1, - timeWindowUnits: 3 - } - setPQBrowserParams(queryString.stringify(queryParams)) - - }, [eventData]) - - return ( - <> - {eventData ? -
    -
    - - - - - - - - - {(eventData.StartTime ? : null)} - {(eventData.Phase ? : null)} - {(eventData.DurationPeriod ? : null)} - {(eventData.Magnitude ? : null)} - {(eventData.SagDepth ? : null)} - {(eventData.BreakerNumber ? : null)} - {(eventData.BreakerTiming ? : null)} - {(eventData.BreakerSpeed ? : null)} - {(eventData.BreakerOperation ? : null)} - - {} - - - -
    Meter:{eventData.MeterName}
    Station:{eventData.StationName}
    Asset:{eventData.AssetName}
    Event Type:{(eventData.EventName != 'Fault' ? eventData.EventName : window.open("./FaultSpecifics.aspx?eventid=" + eventID, eventID + - "FaultLocation", "left=0,top=0,width=350,height=300,status=no,resizable=yes,scrollbars=yes,toolbar=no,menubar=no,location=no")} - >Fault)}
    Event Date:{eventData.EventDate}
    Inception:{moment(eventData.Inception).format('YYYY-MM-DD HH:mm:ss.SSS')}
    Event Start:{eventData.StartTime}
    Phase:{eventData.Phase}
    Duration:{eventData.DurationPeriod}
    Magnitude:{eventData.Magnitude}
    Sag Depth:{eventData.SagDepth}
    Breaker:{eventData.BreakerNumber}
    Timing:{eventData.BreakerTiming}
    Speed:{eventData.BreakerSpeed}
    Operation:{eventData.BreakerOperation}
    -
    -
    : - null} - - ) -} -export default EventInfo; \ No newline at end of file diff --git a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/FFTTable.tsx b/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/FFTTable.tsx deleted file mode 100644 index 1be3b5cd..00000000 --- a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/FFTTable.tsx +++ /dev/null @@ -1,87 +0,0 @@ -//****************************************************************************************************** -// FFTTable.tsx - Gbtc -// -// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 05/14/2018 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - -import * as React from 'react'; -import { useSelector } from 'react-redux'; -import { SelectFFTData } from '../store/dataSlice'; - - -const FFTTable = () => { - const fftPoints = useSelector(SelectFFTData); - - const showAng = (index, row) => { - let f = fftPoints[index].PhaseUnit != undefined ? fftPoints[index].PhaseUnit.factor : 1.0; - let val = fftPoints[index].Angle[row] * f; - return isNaN(val) ? N/A : {val.toFixed(2)}; - }; - - - const showMag = (index, row) => { - let f = (fftPoints?.[index]?.Unit?.factor === undefined ? 1.0 / fftPoints?.[index]?.BaseValue : fftPoints[index]?.Unit?.factor); - let val = fftPoints?.[index]?.Magnitude[row] * f; - return isNaN(val) ? N/A : {val.toFixed(2)}; - }; - - return ( - <> - {fftPoints.length > 0 ? -
    - - - - - {fftPoints.map((item, index) => ( - - ))} - - - - {fftPoints.map((item, index) => ( - - - - - ))} - - - - {fftPoints[0].Angle.map((a, row) => ( - - - {fftPoints.map((_, index) => ( - - {showMag(index, row)} - {showAng(index, row)} - - ))} - - ))} - -
    {item.Asset} {item.Phase}
    Harmonic [Hz]Mag ({item?.Unit?.short})Ang ({item?.PhaseUnit?.short})
    {(row > 0 ? fftPoints[0].Frequency[row].toFixed(2) : 'DC')}
    -
    - : null} - - ); -} - -export default FFTTable; diff --git a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/HarmonicStats.tsx b/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/HarmonicStats.tsx deleted file mode 100644 index 0b30f2e0..00000000 --- a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/HarmonicStats.tsx +++ /dev/null @@ -1,115 +0,0 @@ -//****************************************************************************************************** -// HarmonicStats.tsx - Gbtc -// -// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 05/14/2018 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - -import * as React from 'react'; -import { useAppSelector } from '../hooks'; -import { SelectEventID } from '../store/eventInfoSlice' - -interface Iprops { - exportCallback: () => void, -} - -const HarmonicStatsWidget = (props: Iprops) => { - const [tblData, setTblData] = React.useState>([]); - const evtID = useAppSelector(SelectEventID); - - React.useEffect(() => { - let handle = getData(); - - return () => { if (handle != undefined && handle.abort != undefined) handle.abort(); } - }, [evtID]) - - function getData(): JQuery.jqXHR { - let handle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenSEE/GetHarmonics?eventId=${evtID}`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: true, - async: true - }); - - handle.done((data) => { - let rows = []; - rows.push( - - - {data.map((key,i) => {key.Channel})} - ) - - rows.push( - - Harmonic - {data.map((item, index) => Mag Ang )} - ) - - - let numChannels = data.length; - let jsons = data.map(x => JSON.parse(x.SpectralData)); - let numHarmonics = Math.max(...jsons.map(x => Object.keys(x).length)); - - for (var index = 1; index <= numHarmonics; ++index) { - let tds = []; - let label = 'H' + index - for (let j = 0; j < numChannels; ++j) { - let key = data[j].Channel + label - if (jsons[j][label] != undefined) { - tds.push({jsons[j][label].Magnitude.toFixed(2)}); - tds.push({jsons[j][label].Angle.toFixed(2)}); - } - else { - tds.push(); - tds.push(); - } - } - rows.push( - - {label} - {tds} - ); - } - setTblData(rows); - }); - - return handle; - } - - return ( -
    - - - {tblData[0]} - {tblData[1]} - - - {tblData.slice(2)} - -
    -
    - ); - -} - -export default HarmonicStatsWidget; - - diff --git a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/PhasorChart.tsx b/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/PhasorChart.tsx deleted file mode 100644 index eac08480..00000000 --- a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/PhasorChart.tsx +++ /dev/null @@ -1,161 +0,0 @@ -//****************************************************************************************************** -// PolarChart.tsx - Gbtc -// -// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 05/10/2018 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - -import * as React from 'react'; - -import { useSelector } from 'react-redux'; -import { SelectVPhases, SelectIPhases } from '../store/dataSlice'; -import { SelectColor } from '../store/settingSlice'; -import * as _ from 'lodash'; -import { OpenSee } from '../global'; -import HoverContext from '../Context/HoverContext' - - -const PhasorChartWidget = () => { - const hover = React.useContext(HoverContext); - - const VVector = useSelector(SelectVPhases(hover.hover)); - const IVector = useSelector(SelectIPhases(hover.hover)); - const colors = useSelector(SelectColor); - - const [AssetList, setAssetList] = React.useState([]); - const [scaleV, setScaleV] = React.useState(0); - const [scaleI, setScaleI] = React.useState(0); - - const svgRef = React.useRef(null); - const [svgSize, setSvgSize] = React.useState({ width: 0, height: 0 }); - - React.useLayoutEffect(() => { - if (svgRef.current) - setSvgSize({ width: svgRef.current.clientWidth, height: svgRef.current.clientHeight }); - }, []) - - React.useEffect(() => { - const timeoutId = setTimeout(() => { - const newAssetList = _.uniq([...VVector.map(item => item.Asset), ...IVector.map(item => item.Asset)]); - if (!_.isEqual(newAssetList.sort(), AssetList.sort())) { - setAssetList(newAssetList); - } - setScaleV(0.9 * Math.max(svgSize.width / 2, svgSize.height / 2) / Math.max(...VVector.map(item => item.Magnitude))) - setScaleI(0.9 * Math.max(svgSize.width / 2, svgSize.height / 2) / Math.max(...IVector.map(item => item.Magnitude))) - }, 100); - - return () => clearTimeout(timeoutId); - }, [VVector, IVector]); - - function drawVectorSVG(vec, scale) { - if (vec.Magnitude === undefined || scale === undefined) return ''; - - const centerX = svgSize.width / 2; - const centerY = svgSize.height / 2; - - let x = vec.Magnitude * scale * Math.cos(vec.Angle * Math.PI / 180); - let y = vec.Magnitude * scale * Math.sin(vec.Angle * Math.PI / 180); - - - return `M ${centerX} ${centerY} L ${centerX + x} ${centerY - y} Z`; - } - - - function createTable(vec, index) { - if (vec == undefined) - return N/AN/A - - const factor = (vec.Unit.factor === undefined ? (1.0 / vec.BaseValue) : vec.Unit.factor); - - return ( - {(vec.Magnitude * factor).toFixed(2)} - {(vec.Angle * vec.PhaseUnit.factor).toFixed(2)} - ) - } - - - let rowSpan = VVector.length + IVector.length + 1 - - let MagUnits = _.uniq([...VVector.map((d: OpenSee.IVector) => d.Unit.short), ...IVector.map((d: OpenSee.IVector) => d.Unit.short)]).map((s: string) => { - return "[" + s + "]"; - }).join(" "); - - - let PhaseUnits = _.uniq([...VVector.map((d: OpenSee.IVector) => d.PhaseUnit)]).map((unit: OpenSee.iUnitOptions) => { - return "[" + unit.short + "]"; - }).join(" "); - - const radius = (Math.min(svgSize.width, svgSize.height) / 2) - 10; - - return ( -
    -
    -
    - - - - - - {VVector.map((v, i) => )} - {IVector.map((v, i) => )} - -
    -
    - - - {AssetList.map((item, index) => )} - - - - {AssetList.map(item => )} - - {VVector?.length > 0 ? - VVector.map((vv, index) => ( - - - - - {createTable(vv, index)} - - - )) - : null} - {IVector?.length > 0 ? - IVector.map((iv, index) => ( - - - - - {createTable(iv, index)} - - - )) - : null} - - -
    Mag{MagUnits}Ang{PhaseUnits}
    {item}
       {vv.Color}
       {iv.Color}
    -
    -
    -
    - ) -} - - - -export default PhasorChartWidget; diff --git a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/ScalarStats.tsx b/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/ScalarStats.tsx deleted file mode 100644 index 38441a1e..00000000 --- a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/ScalarStats.tsx +++ /dev/null @@ -1,109 +0,0 @@ -//****************************************************************************************************** -// ScalarStats.tsx - Gbtc -// -// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 05/14/2018 - Billy Ernest -// Generated original version of source code. -// -// 12/07/2023 - Preston Crawford -// Switched table elements to a gpa-gemstone component -//****************************************************************************************************** - -import * as React from 'react'; -import Table from '@gpa-gemstone/react-table' - -interface Iprops { - exportCallback: () => void, -} - -interface EventData { - Stat: string, - Value: string -} - -const ScalarStatsWidget = (props: Iprops) => { - const [stats, setStats] = React.useState([]); - - React.useEffect(() => { - let handle = getData(); - - return () => { if (handle != undefined && handle.abort != undefined) handle.abort(); } - }, [props]) - - function getData(): JQuery.jqXHR { - - let handle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenSEE/GetScalarStats?eventId=${eventID}`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: true, - async: true - }); - - handle.done((d) => { - let t = [] - Object.keys(d).forEach(stat => { - t.push({ Stat: stat, Value: d[stat] }) - }) - setStats(t); - }) - return handle; - } - - return ( - <> -
    -
    - - cols={[ - { - field: 'Stat', - key: 'Stat', - label: 'Stat', - headerStyle: { width: 'calc(30% - 8.25em - 130px)' }, - rowStyle: { width: 'calc(30% - 8.25em - 130px)' }, - }, - { - field: 'Value', - key: 'Value', - label: 'Value', - headerStyle: { width: 'calc(60% - 8.25em)' }, - rowStyle: { width: 'calc(60% - 8.25em)' }, - }, - { - key: 'Export', - label: , - headerStyle: { width: 'calc(60% - 8.25em)' }, - rowStyle: { width: 'calc(60% - 8.25em)' }, - }, - ]} - tableClass="table table-hover w-100" - data={stats} - sortKey={""} - ascending={true} - onSort={() => { }} - onClick={() => { }} - selected={() => false} - /> -
    -
    - - ); -} - -export default ScalarStatsWidget; \ No newline at end of file diff --git a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/SettingWindow.tsx b/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/SettingWindow.tsx deleted file mode 100644 index f89cb93d..00000000 --- a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/SettingWindow.tsx +++ /dev/null @@ -1,718 +0,0 @@ -//****************************************************************************************************** -// SettingWindow.tsx - 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 06/08/2020 - C. Lackner -// Generated original version of source code. -// -// 01/26/2024 - Preston Crawford -// Cleaned up layout and introduced manual time&y limits -//****************************************************************************************************** - -import * as React from 'react'; -import moment from 'moment' -import * as _ from 'lodash'; -import { BlockPicker } from 'react-color'; -import { OpenSee } from '../global'; -import { - SelectData, SelectPlotKeys, SetUnit, SelectIsManual, SetIsManual, SelectOverlappingEvents, - SelectYLimits, SetManualLimits, SelectStartTime, SelectEndTime, - SetTimeLimit, SelectAutoUnits, SelectIsOverlappingManual, - SelectOverlappingYLimits, SelectAxisSettings, SelectOverlappingAutoUnits, -} from '../store/dataSlice'; - -import { - SelectColor, SetColor, SelectTimeUnit, SelectDefaultTraces, SelectPlotMarkers, SetPlotMarkers, - SetDefaultTrace, SelectVTypeDefault, SetDefaultVType, SelectSinglePlot, SelectOverlappingWaveTimeUnit, - SetOverlappingWaveTimeUnit, SetTimeUnit -} from '../store/settingSlice'; - -import { SelectEventInfo, SelectEventID } from '../store/eventInfoSlice' - -import { GetDisplayLabel } from '../Graphs/Utilities'; -import { defaultSettings } from '../defaults'; -import { useAppDispatch, useAppSelector } from '../hooks'; - -import { DatePicker, Input, CheckBox, ColorPicker } from '@gpa-gemstone/react-forms' - -interface TimeLimit { - start: string, - end: string -} - -const SettingsWidget = (props) => { - const dispatch = useAppDispatch(); - const plotKeys = useAppSelector(SelectPlotKeys); - const defaultTraces = useAppSelector(SelectDefaultTraces); - const defaultVtype = useAppSelector(SelectVTypeDefault); - const plotMarkers = useAppSelector(SelectPlotMarkers); - - const startTime = useAppSelector(SelectStartTime); - const endTime = useAppSelector(SelectEndTime); - - const timeUnit = useAppSelector(SelectTimeUnit); - const eventInfo = useAppSelector(SelectEventInfo); - const evtID = useAppSelector(SelectEventID); - const originalStartTime = new Date(eventInfo?.EventDate + "Z").getTime() - const inceptionOffset = (eventInfo?.Inception - originalStartTime) - - const [startMS, setStartMS] = React.useState(startTime - originalStartTime); - const [endMS, setEndMS] = React.useState(endTime - originalStartTime); - - const [scrollOffset, setScrollOffset] = React.useState(0); - const [formattedTime, setFormattedTime] = React.useState({ start: '', end: '' }); - const [currentDate, setCurrentDate] = React.useState<{ start: Date, end: Date }>({ start: new Date(), end: new Date() }); - - const [valid, setValid] = React.useState(true) - const [timeSinceChanged, setTimeSinceChanged] = React.useState(false) - - const handleTimeChange = (time: number, start: boolean) => { - if (start) - setStartMS(time) - else - setEndMS(time) - if (!timeSinceChanged) - setTimeSinceChanged(true) - } - - const handleTimeUnitChange = (index: number) => { - dispatch(SetTimeUnit({ index: index })) - } - - const handleDateChange = (time, start: boolean) => { - let newDate: Date; - if (start) - newDate = new Date(startTime); - else - newDate = new Date(endTime); - - if (time && time !== 'Invalid date') { - const timeString = time.split(':'); - const [hours, minutes] = [timeString[0], timeString[1]]; - let seconds = timeString[2]; - let milliseconds = ''; - [seconds, milliseconds] = seconds.split('.') - newDate.setHours(parseInt(hours), parseInt(minutes), parseInt(seconds), parseInt(milliseconds)) - - if (start) { - if (newDate.getTime() < endTime) { - setValid(true) - setFormattedTime({ start: moment(newDate).format('HH:mm:ss.SSS'), end: moment(new Date(endTime)).format('HH:mm:ss.SSS') }) - setCurrentDate({ start: newDate, end: new Date(endTime) }) - } - else - setValid(false) - } - else { - if (newDate.getTime() > startTime) { - setFormattedTime({ start: moment(new Date(startTime)).format('HH:mm:ss.SSS'), end: moment(newDate).format('HH:mm:ss.SSS') }) - setCurrentDate({ start: new Date(startTime), end: newDate }) - setValid(true) - } - else - setValid(false) - } - - } - - }; - - React.useEffect(() => { - const startDate = new Date(startTime) - const endDate = new Date(endTime) - setFormattedTime({ start: moment(startDate).format('HH:mm:ss.SSS'), end: moment(new Date(endDate)).format('HH:mm:ss.SSS') }) - setCurrentDate({ start: startDate, end: endDate }) - }, []) - - //Effect to update start and end time whenever formattedTime changes - React.useEffect(() => { - const timeOutId = setTimeout(() => { - - if (defaultSettings.TimeUnit.options[timeUnit.current].short.includes('since')) { - const isCycles = defaultSettings.TimeUnit.options[timeUnit.current].short.includes('cycles') - const isSinceInception = defaultSettings.TimeUnit.options[timeUnit.current].short.includes('inception') - const curStartMS = isCycles ? startMS / (60.0 / 1000.0) : startMS - const curEndMS = isCycles ? endMS / (60.0 / 1000.0) : endMS - const newStartTime = isSinceInception ? originalStartTime + curStartMS + inceptionOffset : originalStartTime + curStartMS - const endOffset = curEndMS - (endTime - originalStartTime) - const newEndTime = isSinceInception ? endTime + endOffset + inceptionOffset : endTime + endOffset - - - if (newStartTime !== startTime && timeSinceChanged) - dispatch(SetTimeLimit({ start: newStartTime, end: endTime })); - - - if (newEndTime !== endTime && timeSinceChanged) - dispatch(SetTimeLimit({ start: startTime, end: newEndTime })); - } - }, 1000); - return () => clearTimeout(timeOutId); - - }, [startMS, endMS]); - - - //Effect to update - React.useEffect(() => { - const timeOutId = setTimeout(() => { - if (defaultSettings.TimeUnit.options[timeUnit.current].short.includes('since')) { - const isCycles = defaultSettings.TimeUnit.options[timeUnit.current].short.includes('cycles') - const isSinceInception = defaultSettings.TimeUnit.options[timeUnit.current].short.includes('inception') - const newStartMS = isSinceInception ? startTime - originalStartTime - inceptionOffset : startTime - originalStartTime - const newEndMS = isSinceInception ? endTime - originalStartTime - inceptionOffset : endTime - originalStartTime - - if (isCycles) { - const newStartCycles = newStartMS * 60.0 / 1000.0 - const newEndCycles = newEndMS * 60.0 / 1000.0 - - if (Math.abs(newStartCycles - startMS) > 0.1) - setStartMS(newStartCycles) - if (Math.abs(newEndCycles - endMS) > 0.1) - setEndMS(newEndCycles) - } - - else { - setStartMS(newStartMS) - setEndMS(newEndMS) - - } - - } - - }, 1000); - return () => clearTimeout(timeOutId); - }, [startTime, endTime, timeUnit]); - - - //Effect to update start and end time whenever formattedTime changes - React.useEffect(() => { - const newStart = currentDate.start.getTime(); - const newEnd = currentDate.end.getTime(); - - if (newEnd - newStart !== endTime - startTime && valid && !defaultSettings.TimeUnit.options[timeUnit.current].short.includes("since")) { - const timeOutId = setTimeout(() => { - dispatch(SetTimeLimit({ start: newStart, end: newEnd })); - }, 1000); - - return () => clearTimeout(timeOutId); - } - }, [formattedTime]); - - - React.useEffect(() => { - const handleScroll = () => { - const offset = document.getElementById("settingScrollContainer").scrollTop; - setScrollOffset(offset); - } - document.getElementById("settingScrollContainer").addEventListener("scroll", handleScroll, { passive: true }); - return () => { if (document.getElementById("settingScrollContainer") != null) document.getElementById("settingScrollContainer").removeEventListener("scroll", handleScroll); } - }, [props]) - - - return ( -
    -
    -
    -
    -
    -

    - -

    -
    -
    -
    -
    - Default Traces (on Loading): -
    -
    - dispatch(SetDefaultTrace({ ...defaultTraces, W: item.W }))} - Label={"WaveForm"} - /> -
    -
    - dispatch(SetDefaultTrace({ ...defaultTraces, Pk: item.Pk }))} - Label={"Peak"} - /> -
    -
    - dispatch(SetDefaultTrace({ ...defaultTraces, RMS: item.RMS }))} - Label={"RMS"} - /> -
    -
    - dispatch(SetDefaultTrace({ ...defaultTraces, Ph: item.Ph }))} - Label={"Phase"} - /> -
    -
    -
    -
    -
    - { - if (defaultVtype == 'L-N') - dispatch(SetDefaultVType('L-L')) - }} /> - -
    -
    -
    -
    - - { - if (defaultVtype == 'L-L') - dispatch(SetDefaultVType('L-N')) - }} /> - -
    -
    -
    -
    -
    - Time: -
    -
    - {props.DataType != 'FFT' ? handleTimeUnitChange(index)} /> : null} -
    -
    - {defaultSettings.TimeUnit.options[timeUnit.current].short.includes("since") ? -
    -
    - handleTimeChange(start.startMS, true)} - Field={"startMS"} - Valid={() => true} - Label={"Start"} - Type={"number"} - /> -
    -
    - handleTimeChange(end.endMS, false)} - Field={"endMS"} - Valid={() => true} - Label={"End"} - Type={"number"} - /> -
    -
    : -
    -
    - Record={formattedTime} Format={"HH:mm:ss.SSS"} Field={'start'} - Setter={(e) => { - handleDateChange(e.start, true) - }} - Label={"Start Time"} - Accuracy={'millisecond'} - Valid={() => valid} - Type={'time'} - Feedback={"Start Time can not be greater than End Time"} - /> -
    - -
    - Record={formattedTime} Format={"HH:mm:ss.SSS"} Field={'end'} - Setter={(e) => { - handleDateChange(e.end, false) - }} - Label={"End Time"} - Valid={() => valid} - Type={'time'} - Accuracy={'millisecond'} - Feedback={"Start Time can not be greater than End Time"} - /> -
    - -
    - } -
    -
    - Plot Markers: -
    -
    - dispatch(SetPlotMarkers(item.plotMarkers))} - Label={"Display Inception and Duration."} - Help={"For events without this information record start and end time will be used."} - /> -
    -
    -
    -
    - -
    -
    - - {plotKeys.filter(key => key.EventId === evtID || key.EventId === -1).map((item, index) => )} - -
    -
    -
    - ); -} - -export default SettingsWidget; - -export const AxisUnitSelector = (props: { label: string, setter: (index: number) => void, unitType: OpenSee.Unit, axisSetting: OpenSee.IAxisSettings }) => { - let entries; - let buttonLabel; - - entries = defaultSettings.Units[props.unitType].options.map((option, index) => - { props.setter(index) }}> {option.label} - ) - if (props.axisSetting.isAuto) - buttonLabel = props.label + " [auto]" - else - buttonLabel = props.label + " [" + defaultSettings.Units[props.unitType].options[props.axisSetting.current].short + "]" - - - - return ( -
    - -
    - {entries} -
    -
    - ); -} - -export const TimeUnitSelector = (props: { label: string, setter: (index: number) => void, timeUnitIndex: number, overlappingWave?: boolean }) => { - let entries; - let buttonLabel; - - if (props.overlappingWave) { - entries = defaultSettings.OverlappingWaveTimeUnit.options.map((option, index) => - { props.setter(index) }}> {option.label} - ) - buttonLabel = props.label + " [" + defaultSettings.OverlappingWaveTimeUnit.options[props.timeUnitIndex].short + "]" - - } else { - entries = defaultSettings.TimeUnit.options.map((option, index) => - { props.setter(index) }}> {option.label} - ) - buttonLabel = props.label + " [" + defaultSettings.TimeUnit.options[props.timeUnitIndex].short + "]" - - } - - return ( -
    - -
    - {entries} -
    -
    - ); -} - -interface ICardProps extends OpenSee.IGraphProps { scrollOffset: number } -interface ILimits { - min: number, - max: number -} - -const PlotCard = (props: ICardProps) => { - const dispatch = useAppDispatch(); - - const MemoSelectYlimits = React.useMemo(() => SelectYLimits(props), [props]); - const yLimits = useAppSelector(MemoSelectYlimits); - - const MemoSelectOverLappingYLimits = React.useMemo(() => SelectOverlappingYLimits(props.DataType), [props]); - const overlappingYLimits = useAppSelector(MemoSelectOverLappingYLimits); - - const MemoSelectData = React.useMemo(() => SelectData(props), []); - const lineData = useAppSelector(MemoSelectData); - - const singlePlot = useAppSelector(SelectSinglePlot); - const axisSettings = useAppSelector(SelectAxisSettings(props)); - const colors = useAppSelector(SelectColor); - - const overlappingKeys = useAppSelector(SelectOverlappingEvents(props.DataType)); - const overlapWaveTimeUnit = useAppSelector(SelectOverlappingWaveTimeUnit); - - const isManual = useAppSelector(SelectIsManual(props)); - const isOverlappingManual = useAppSelector(SelectIsOverlappingManual(props.DataType)); - const isOverlappingAuto = useAppSelector(SelectOverlappingAutoUnits(props.DataType)); - - const [curLimits, setCurLimits] = React.useState>(null); - const [overlappingLimits, setOverlappingLimits] = React.useState>(null) - - const [limitsPayload, setLimitsPayload] = React.useState<{ axis: OpenSee.Unit, limits: [number, number], key: OpenSee.IGraphProps, auto: boolean, factor: number }>(null); - - const [valid, setValid] = React.useState(true) - - const autoUnits = useAppSelector(SelectAutoUnits(props)); - - let colorSettings: OpenSee.Color[] = _.uniq(lineData.map((item: OpenSee.iD3DataSeries) => item.Color as OpenSee.Color)); - let unitSettings: OpenSee.Unit[] = _.uniq(lineData.map((item: OpenSee.iD3DataSeries) => item.Unit)); - - React.useEffect(() => { - if (limitsPayload?.limits) { - const timeOutId = setTimeout(() => { - - if (limitsPayload.limits[0] < limitsPayload.limits[1]) { - setValid(true) - dispatch(SetManualLimits(limitsPayload)) - } - else - setValid(false) - }, 1500) - setLimitsPayload(null); - return () => clearTimeout(timeOutId) - } - }, [curLimits]) - - - const handleLimitChange = (axis: OpenSee.Unit, limits: [number, number], key: OpenSee.IGraphProps, auto: boolean) => { - let limit = { min: null, max: null } - let factor = 1 - if (defaultSettings.Units[axis].options[axisSettings[axis].current].factor !== 1) - factor = defaultSettings.Units[axis].options[axisSettings[axis].current].factor - - limit.min = limits[0] - limit.max = limits[1] - setCurLimits(prevLimits => ({ ...prevLimits, [axis]: limit })); - setLimitsPayload({ axis, limits, key, auto, factor }) - } - - const handleUnitChange = (unit: OpenSee.Unit, index: number, key: OpenSee.IGraphProps) => { - let auto: boolean; - if (defaultSettings.Units[unit].options[index].factor === 0) - auto = true - else - auto = false - dispatch(SetUnit({ unit: unit, value: index, auto: auto, key: key })) - } - - const getLabel = (unit: OpenSee.Unit, key?: OpenSee.IGraphProps) => { - - if (key) { - if (isOverlappingAuto?.[key.DataType]?.[unit] && isOverlappingManual?.[key.DataType]?.[unit]) { - let settingOptions: OpenSee.iUnitOptions[] = defaultSettings.Units[unit].options - let index = settingOptions.findIndex(item => item.factor === 1) - return settingOptions[index].short - } - else - return defaultSettings.Units[unit].options[axisSettings[unit].current].short - } - - else if (isManual[unit] && autoUnits[unit]) { - let settingOptions: OpenSee.iUnitOptions[] = defaultSettings.Units[unit].options - let index = settingOptions.findIndex(item => item.factor === 1) - return settingOptions[index].short - } - else - return defaultSettings.Units[unit].options[axisSettings[unit].current].short - } - - React.useEffect(() => { - const limits = getYlimits(yLimits) - setCurLimits(limits as OpenSee.IUnitCollection) - - if (overlappingYLimits) { - let overlappingLimits = {} - - Object.keys(overlappingYLimits).forEach(graphType => { - const yLimits = overlappingYLimits[graphType as OpenSee.graphType]; - const limits = getYlimits(yLimits, graphType); - overlappingLimits[graphType] = limits - }); - - setOverlappingLimits(overlappingLimits as OpenSee.IGraphCollection) - } - - }, [yLimits, overlappingYLimits]) - - function getYlimits(yLimits, graphType?) { - let limits = {} - Object.keys(yLimits).forEach(unit => { - limits[unit] = {}; - let factor = 1 - let autoUnit = autoUnits[unit] - const overLappingManual = isOverlappingManual?.[graphType]?.[unit] === undefined ? false : isOverlappingManual[graphType][unit] - - if (overLappingManual || isManual[unit]) { - if (defaultSettings.Units[unit].options[axisSettings[unit].current].factor !== 1) - factor = defaultSettings.Units[unit].options[axisSettings[unit].current].factor - - limits[unit].min = yLimits?.[unit][0] - limits[unit].max = yLimits?.[unit][1] - - if (autoUnit) { - limits[unit].min = limits[unit].min / factor - limits[unit].max = limits[unit].max / factor - } - } - - }) - return limits - } - - const handleTimeUnitChange = (index: number) => { - dispatch(SetOverlappingWaveTimeUnit(index)) - } - - return (
    -
    -

    - -

    -
    - -
    -
    - {unitSettings.map(item => ( -
    - {item} -
    -
    - handleUnitChange(item, index, props)} unitType={item} axisSetting={axisSettings[item]} /> -
    -
    - dispatch(SetIsManual({ key: props, unit: item, manual: !e.target.checked }))} /> - -
    -
    - dispatch(SetIsManual({ key: props, unit: item, manual: e.target.checked }))} /> - -
    -
    - - {isManual[item] && ( - <> -
    -
    - Record={curLimits?.[item] ? curLimits?.[item] : { min: 0, max: 1 }} Field={'min'} Setter={(limits) => handleLimitChange(item, [limits.min, limits.max], props, autoUnits[item])} - Valid={() => valid} - Label={`${item} ` + `Min [${getLabel(item)}]`} - Type={'number'} - Help={autoUnits[item] ? 'When Auto Unit is selected manual limits are in the base unit (e.g., volts)' : undefined} - Feedback={"Minimum limit can not be greater than Maximum limit"} - /> -
    -
    - Record={curLimits?.[item] ? curLimits?.[item] : { min: 0, max: 1 }} Field={'max'} Setter={(limits) => handleLimitChange(item, [limits.min, limits.max], props, autoUnits[item])} - Valid={() => valid} - Label={`${item} ` + `Max [${getLabel(item)}]`} - Type={'number'} - Help={autoUnits[item] ? 'When Auto Unit is selected manual limits are in the base unit (e.g., volts)' : undefined} - Feedback={"Minimum limit can not be greater than Maximum limit"} - /> -
    -
    - - )} - {overlappingKeys.length > 0 && !singlePlot ? - overlappingKeys.map((key, idx) => ( -
    -
    -

    Overlapping Event {idx + 1}

    -
    -
    - dispatch(SetIsManual({ key: key, unit: item, manual: !e.target.checked }))} /> - -
    -
    - dispatch(SetIsManual({ key: key, unit: item, manual: e.target.checked }))} /> - -
    - - {isOverlappingManual?.[key.DataType]?.[item] && ( - <> -
    -
    - Record={overlappingLimits?.[key.DataType]?.[item] ? overlappingLimits?.[key.DataType]?.[item] : { min: 0, max: 1 }} Field={'min'} Setter={(limits) => handleLimitChange(item, [limits.min, limits.max], key, autoUnits[item])} - Valid={() => valid} - Label={`${item} ` + `Min [${getLabel(item, key)}]`} - Type={'number'} - Help={autoUnits[item] ? 'When Auto Unit is selected limits will be factored to the base unit' : undefined} - Feedback={"Minimum limit can not be greater than Maximum limit"} - /> -
    -
    - Record={overlappingLimits?.[key.DataType]?.[item] ? overlappingLimits?.[key.DataType]?.[item] : { min: 0, max: 1 }} Field={'max'} Setter={(limits) => handleLimitChange(item, [limits.min, limits.max], key, autoUnits[item])} - Valid={() => valid} - Label={`${item} ` + `Max [${getLabel(item, key)}]`} - Type={'number'} - Help={autoUnits[item] ? 'When Auto Unit is selected limits will be factored to the base unit' : undefined} - Feedback={"Minimum limit can not be greater than Maximum limit"} - /> -
    -
    - - )} -
    - )) : null} -
    - ))} - - {colorSettings.length > 0 ? -
    - Colors: -
    - {colorSettings.map((c: OpenSee.Color, i: number) => -
    - - Record={colors} - Field={c} - key={i} - Label={c as string} - Setter={(col) => dispatch(SetColor({ color: c, value: col[c] }))} - Style={{ background: colors[c], marginBottom: 5 }} - /> -
    )} -
    -
    : null} - - - {props.DataType === "OverlappingWave" ? -
    - Time: -
    -
    - handleTimeUnitChange(index)} overlappingWave={true} /> -
    -
    -
    - : null} -
    -
    -
    ); - -} diff --git a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/Tooltip.tsx b/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/Tooltip.tsx deleted file mode 100644 index 41872644..00000000 --- a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/Tooltip.tsx +++ /dev/null @@ -1,64 +0,0 @@ -//****************************************************************************************************** -// Tooltip.tsx - Gbtc -// -// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 05/14/2018 - Billy Ernest -// Generated original version of source code. -// -// 05/14/2018 - Preston Crawford -// Updated layout -//****************************************************************************************************** - -import * as React from 'react'; -import moment = require('moment'); -import { useSelector } from 'react-redux'; -import { SelectHoverPoints } from '../store/dataSlice'; -import { SelectColor } from '../store/settingSlice'; -import HoverContext from '../Context/HoverContext' - - -const ToolTipWidget = () => { - const hover = React.useContext(HoverContext); - const points = useSelector(SelectHoverPoints(hover.hover)); - const colors = useSelector(SelectColor); - - return ( -
    - - - - - - {points.map((p, i) => - - - - - )} - -
    - {moment(hover.hover[0]).utc().format("MM-DD-YYYY HH:mm:ss.SSSSSS")} -
        - {p.Name} - - {(p.Value * (p.Unit.factor === undefined ? 1.0 / p.BaseValue : p.Unit.factor)).toFixed(2)} ({p.Unit.short}) -
    -
    - ) -} - -export default ToolTipWidget diff --git a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/TooltipWithDelta.tsx b/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/TooltipWithDelta.tsx deleted file mode 100644 index 0bba131a..00000000 --- a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/TooltipWithDelta.tsx +++ /dev/null @@ -1,82 +0,0 @@ -//****************************************************************************************************** -// Tooltip.tsx - Gbtc -// -// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 05/14/2018 - Billy Ernest -// Generated original version of source code. -// -// 01/24/2024 - Preston Crawford -// Refactored layout -// -//****************************************************************************************************** - -import * as React from 'react'; -import moment = require('moment'); -import { SelectDeltaHoverPoints } from '../store/dataSlice'; -import { SelectColor } from '../store/settingSlice'; -import { useAppSelector } from '../hooks'; -import HoverContext from '../Context/HoverContext' - -const ToolTipDeltaWidget = () => { - const hover = React.useContext(HoverContext); - const points = useAppSelector(SelectDeltaHoverPoints(hover.hover)); - const colors = useAppSelector(SelectColor); - - let data: Array = (points.map((p, i) => -     - {p.Name} - {(p.Value * (p.Unit.factor === undefined ? 1.0 / p.BaseValue : p.Unit.factor)).toFixed(2)} ({p.Unit.short}) - {(p.PrevValue * (p.Unit.factor === undefined ? 1.0 / p.BaseValue : p.Unit.factor)).toFixed(2)} ({p.Unit.short}) - {((p.Value - p.PrevValue) * (p.Unit.factor === undefined ? 1.0 / p.BaseValue : p.Unit.factor)).toFixed(2)} ({p.Unit.short}) - )) - - - let firstDate = hover.hover[0]; - let secondDate = points.length > 0 ? points[0].Time : NaN; - - return ( -
    -
    - - - - - - {!isNaN(secondDate) ? <> - - - : } - - - - {data} - -
    - {(!isNaN(firstDate) ? moment(firstDate).utc().format("HH:mm:ss.SSSSSS") : null)} - - {(moment(secondDate).utc().format("HH:mm:ss.SSSSSS"))} - - {(!isNaN(firstDate) ? ((secondDate - firstDate) / 1000).toFixed(9) + ' (s)' : '')} - - Select a Point -
    -
    -
    - ) -} - -export default ToolTipDeltaWidget; diff --git a/src/OpenSEE/Scripts/TSX/openSEE.tsx b/src/OpenSEE/Scripts/TSX/openSEE.tsx deleted file mode 100644 index 939fb990..00000000 --- a/src/OpenSEE/Scripts/TSX/openSEE.tsx +++ /dev/null @@ -1,392 +0,0 @@ -//****************************************************************************************************** -// openSEE.tsx - Gbtc -// -// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 04/17/2018 - Billy Ernest -// Generated original version of source code. -// 08/22/2019 - Christoph Lackner -// Added TCE Plot. -// -//****************************************************************************************************** - -// To-DO: -// # Fix Dowload.ash to include Analytics -// - -import { Application, SplitDrawer, SplitSection, VerticalSplit } from '@gpa-gemstone/react-interactive'; -import moment from 'moment' -import * as React from 'react'; -import * as ReactDOM from 'react-dom/client'; -import { Provider } from 'react-redux'; -import createHistory from "history/createBrowserHistory" - -import * as queryString from "query-string"; -import * as _ from "lodash"; - -import AnalyticOptions from './Components/AnalyticOptions'; -import LineChart from './Graphs/LineChartBase'; -import OpenSeeNavBar from './Components/OpenSEENavbar'; - -import store from './store/store'; -import { sortGraph } from './Graphs/Utilities' -import { OpenSee } from './global'; - -import { LoadSettings, SelectQueryString, SelectMouseMode, SetMouseMode, SelectSinglePlot } from './store/settingSlice'; -import { SelectCycles, UpdateAnalytic, SelectAnalytics } from './store/analyticSlice'; -import { SetTimeLimit, SelectDisplayed, SelectFFTLimits, SelectListGraphs, SelectPlotKeys } from './store/dataSlice'; -import { LoadEventInfo, SetEventID, SelectEventInfo, LoadLookupInfo } from './store/eventInfoSlice' -import { LoadOverlappingEvents, SelectEventList } from './store/overlappingEventsSlice'; - -import OverlappingEventWindow from './Components/OverlappingEvents'; -import BarChart from './Graphs/BarChartBase'; -import { updatedURL } from './store/queryThunk'; -import { useAppDispatch, useAppSelector } from './hooks'; - -import SettingsWidget from './jQueryUI Widgets/SettingWindow'; -import PointWidget from './jQueryUI Widgets/AccumulatedPoints'; -import PhasorChartWidget from './jQueryUI Widgets/PhasorChart'; -import ToolTipWidget from './jQueryUI Widgets/Tooltip'; -import ToolTipDeltaWidget from './jQueryUI Widgets/TooltipWithDelta'; -import ScalarStatsWidget from './jQueryUI Widgets/ScalarStats'; -import TimeCorrelatedSagsWidget from './jQueryUI Widgets/TimeCorrelatedSags'; -import LightningDataWidget from './jQueryUI Widgets/LightningData'; -import FFTTable from './jQueryUI Widgets/FFTTable'; -import EventInfo from './jQueryUI Widgets/EventInfo'; -import HarmonicStatsWidget from './jQueryUI Widgets/HarmonicStats' - -import HoverProvider from './Context/HoverProvider' - -declare var homePath: string; -declare var version: string; -declare var eventID: number; - -const OpenSeeHome = () => { - const applicationRef = React.useRef(null); - const plotRef = React.useRef(null); - const history = React.useRef(createHistory()); - const dispatch = useAppDispatch(); - const overlayHandles = React.useRef({ - Settings: () => { }, - AccumulatedPoints: () => { }, - PolarChart: () => { }, - ScalarStats: () => { }, - CorrelatedSags: () => { }, - Lightning: () => { }, - FFTTable: () => { }, - HarmonicStats: () => { }, - }); - - const [openDrawers, setOpenDrawers] = React.useState({ - Settings: false, - AccumulatedPoints: false, - PolarChart: false, - ScalarStats: false, - CorrelatedSags: false, - Lightning: false, - FFTTable: false, - Info: false, - Compare: false, - Analytics: false, - ToolTip: false, - ToolTipDelta: false, - HarmonicStats: false - }) - - const [resizeCount, setResizeCount] = React.useState(0); - const [plotWidth, setPlotWidth] = React.useState(window.innerWidth - 300); - - const mouseMode = useAppSelector(SelectMouseMode); - - const eventInfo = useAppSelector(SelectEventInfo); - - const groupedKeys = useAppSelector(SelectListGraphs); - const plotKeys = useAppSelector(SelectPlotKeys); - const singlePlot = useAppSelector(SelectSinglePlot); - - const eventList = useAppSelector(SelectEventList); - - const showPlots = useAppSelector(SelectDisplayed); - const cycles = useAppSelector(SelectCycles); - const analytics = useAppSelector(SelectAnalytics); - - const fftTime = useAppSelector(SelectFFTLimits); - const query = useAppSelector(SelectQueryString); - - const [plotHeight, setPlotHeight] = React.useState(250); - const [navWidth, setNavWidth] = React.useState(100); - - React.useLayoutEffect(() => { - const timeoutId = setTimeout(() => { - if (applicationRef.current) { - const newHeight = ((window.innerHeight - applicationRef.current?.navBarDiv?.offsetHeight) / Math.min(plotKeys.length, 3)) - const newWidth = plotRef.current ? plotRef.current.offsetWidth : 0 - const newNavBarWidth = applicationRef.current?.navBarDiv?.offsetWidth - if (newHeight !== plotHeight && !isNaN(newHeight) && isFinite(newHeight)) - setPlotHeight(newHeight) - - if (newWidth !== plotWidth && !isNaN(newWidth) && isFinite(newWidth)) - setPlotWidth(newWidth); - - if (navWidth !== newNavBarWidth && !isNaN(newNavBarWidth) && isFinite(newNavBarWidth)) - setNavWidth(newNavBarWidth) - } - }, 100); - return () => clearTimeout(timeoutId); - }, [plotKeys, openDrawers, resizeCount]) - - - //Effect to handle queryParams - React.useEffect(() => { - const query = queryString.parse(history.current['location'].search); - - const evStart = query['eventStartTime'] != undefined ? query['eventStartTime'] : eventStartTime; - const evEnd = query['eventEndTime'] != undefined ? query['eventEndTime'] : eventEndTime; - - const startTime = (query['startTime'] != undefined ? parseInt(query['startTime']) : new Date(evStart + "Z").getTime()); - const endTime = (query['endTime'] != undefined ? parseInt(query['endTime']) : new Date(evEnd + "Z").getTime()); - - dispatch(LoadEventInfo({ breakeroperation: ""/*not really sure what breakeroperation is..*/ })) - dispatch(SetEventID(eventID)) - dispatch(SetTimeLimit({ start: startTime, end: endTime })); - dispatch(UpdateAnalytic({ settings: { ...analytics, FFTStartTime: startTime } })); - - dispatch(updatedURL({ query: history.current['location'].search, initial: true })); - - history.current['listen'](location => { - // If Query changed then we update states.... - // Note that enabled and selected states that depend on loading state are not dealt with in here - dispatch(updatedURL({ query: location.search, initial: false })); - }); - - }, []); - - //Effect to push updatedQueryParams - React.useEffect(() => { - const timeoutId = setTimeout(() => { - history.current['push'](`?${query}`); - }, 1000); - - return () => clearTimeout(timeoutId); - }, [query]); - - React.useEffect(() => { - window.addEventListener("resize", () => { - setResizeCount(x => x + 1) - }); - return () => { $(window).off('resize'); } - }, []) - - //Effect to update EventID - React.useEffect(() => { - if (eventID && !isNaN(eventID) && eventID !== 0) { - dispatch(SetEventID(eventID)) - dispatch(LoadEventInfo({ breakeroperation: "" })) - dispatch(LoadLookupInfo()) - dispatch(LoadOverlappingEvents()) - } - }, [eventID]); - - React.useEffect(() => { - if (openDrawers.ToolTipDelta) { - let oldMode = _.clone(mouseMode); - dispatch(SetMouseMode('select')) - return () => { dispatch(SetMouseMode(oldMode)) } - } - }, [openDrawers.ToolTipDelta]) - - const ToggleDrawer = (drawer: OpenSee.OverlayDrawers, open: boolean) => { - overlayHandles.current[drawer](open); - }; - - const handleDrawerChange = (drawerName: keyof OpenSee.Drawers, isOpen: boolean) => { - setOpenDrawers(prevStates => ({ ...prevStates, [drawerName]: isOpen })); - }; - - function exportData(type) { - window.open(homePath + `CSVDownload.ashx?type=${type}&eventID=${eventID}` + - `${showPlots.Voltage != undefined ? `&displayVolt=${showPlots.Voltage}` : ``}` + - `${showPlots.Current != undefined ? `&displayCur=${showPlots.Current}` : ``}` + - `${showPlots.TripCoil != undefined ? `&displayTCE=${showPlots.TripCoil}` : ``}` + - `${showPlots.Digitals != undefined ? `&breakerdigitals=${showPlots.Digitals}` : ``}` + - `${showPlots.Analogs != undefined ? `&displayAnalogs=${showPlots.Analogs}` : ``}` + - `${type == 'fft' ? `&startDate=${fftTime[0]}` : ``}` + - `${type == 'fft' ? `&cycles=${cycles}` : ``}` + - `&Meter=${eventInfo.MeterName}` + - `&EventType=${eventInfo.MeterName}` - ); - } - - return ( - } - UseLegacyNavigation={true} - ref={applicationRef} - > - - handleDrawerChange("Info", item)}> - - - - handleDrawerChange("Compare", item)}> - - - - handleDrawerChange("Analytics", item)}> - - - - handleDrawerChange("ToolTip", item)}> - - - - handleDrawerChange("ToolTipDelta", item)} > - - - - { overlayHandles.current.Settings = func; }} ShowClosed={false} - OnChange={(item) => handleDrawerChange("Settings", item)} > - - - - { overlayHandles.current.AccumulatedPoints = func; }} ShowClosed={false} - OnChange={(item) => handleDrawerChange("AccumulatedPoints", item)}> - - - - { overlayHandles.current.ScalarStats = func; }} ShowClosed={false} - OnChange={(item) => handleDrawerChange("ScalarStats", item)}> - exportData('stats')} /> - - - { overlayHandles.current.CorrelatedSags = func; }} ShowClosed={false} - OnChange={(item) => handleDrawerChange("CorrelatedSags", item)}> - exportData('correlatedsags')} /> - - - { overlayHandles.current.Lightning = func; }} ShowClosed={false} - OnChange={(item) => handleDrawerChange("Lightning", item)}> - - - - { overlayHandles.current.FFTTable = func; }} ShowClosed={false} - OnChange={(item) => handleDrawerChange("FFTTable", item)}> - - - - { overlayHandles.current.PolarChart = func; }} ShowClosed={false} - OnChange={(item) => handleDrawerChange("PolarChart", item)}> - - - - { overlayHandles.current.HarmonicStats = func; }} ShowClosed={false} - OnChange={(item) => handleDrawerChange("HarmonicStats", item)}> - exportData('harmonics')} /> - - - -
    - {groupedKeys[eventID] != undefined ? ( - <> - {groupedKeys[eventID].filter(item => item.DataType !== 'FFT').sort(sortGraph).map(item => ( - - ))} - - {groupedKeys[eventID].filter(item => item.DataType === 'FFT').sort(sortGraph).map(item => ( - - ))} - - ) : null} - - {Object.keys(groupedKeys).filter(item => parseInt(item) !== eventID).map(key => -
    - {eventList.find(item => item.EventID === parseInt(key)) ? ( -
    -
    -
    - Meter:
    - {eventList.find(item => item.EventID === parseInt(key)).MeterName} -
    -
    - Asset:
    - {eventList.find(item => item.EventID === parseInt(key)).AssetName} -
    -
    - Type:
    - {eventList.find(item => item.EventID === parseInt(key)).EventType} -
    -
    - Inception:
    - {moment(eventList.find(item => item.EventID === parseInt(key)).Inception).format('YYYY-MM-DD HH:mm:ss.SSS')} -
    -
    -
    - ) : null} -
    - {groupedKeys[key].filter(item => item.DataType !== 'FFT').sort(sortGraph).map(item => ( - - ))} - - {groupedKeys[key].filter(item => item.DataType === 'FFT').sort(sortGraph).map(item => ( - - ))} - -
    -
    - )} - -
    -
    -
    -
    -
    ); -} - -//Load Settings for settings Slice -store.dispatch(LoadSettings()); - -// After -const container = document.getElementById('DockCharts'); -const root = ReactDOM.createRoot(container!); // createRoot(container!) if you use TypeScript -root.render(); - diff --git a/src/OpenSEE/Scripts/TSX/store/RequestHandler.tsx b/src/OpenSEE/Scripts/TSX/store/RequestHandler.tsx deleted file mode 100644 index b694e283..00000000 --- a/src/OpenSEE/Scripts/TSX/store/RequestHandler.tsx +++ /dev/null @@ -1,70 +0,0 @@ -//****************************************************************************************************** -// RequestHandler.tsx - Gbtc -// -// Copyright © 2021, 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 02/05/2020 - C. Lackner -// Generated original version of source code. -// -//****************************************************************************************************** - -import { OpenSee } from "../global"; -var HandleStore = new Map[]>(); - -//Functions to Handle Requests. -function AddRequest(key: OpenSee.IGraphProps, requests: JQuery.jqXHR[]) { - let target = key.DataType.toString() + '-' + key.EventId.toString(); - if (HandleStore.has(target)) - HandleStore.get(target).forEach(item => { if (item != null && item.abort != null) item.abort(); }) - HandleStore.set(target, requests); -} - -function CancelAnalytics() { - for (let key of HandleStore.keys()) { - if (key.startsWith('Voltage-') || key.startsWith('Current-') || key.startsWith("Analogs-") || key.startsWith("Digitals-") || key.startsWith('TripCoil-')) - continue; - HandleStore.get(key).forEach(item => { if (item != null && item.abort != null) item.abort(); }) - HandleStore.delete(key); - } -} - -function CancelCompare(baseEventID: number) { - for (let key of HandleStore.keys()) { - if (key.endsWith('-' + baseEventID.toString())) - continue; - HandleStore.get(key).forEach(item => { if (item != null && item.abort != null) item.abort(); }) - HandleStore.delete(key); - } -} - -function CancelEvent(eventId: number) { - for (let key of HandleStore.keys()) { - if (!key.endsWith('-' + eventId.toString())) - continue; - HandleStore.get(key).forEach(item => { if (item != null && item.abort != null) item.abort(); }) - HandleStore.delete(key); - } -} - -function AppendRequest(key: OpenSee.IGraphProps, requests: JQuery.jqXHR[]) { - let target = key.DataType.toString() + '-' + key.EventId.toString(); - let r = requests; - if (HandleStore.has(target)) - r = [...r, ...HandleStore.get(target)]; - HandleStore.set(target, r) -} - -export { CancelAnalytics, AddRequest, CancelEvent, CancelCompare, AppendRequest } \ No newline at end of file diff --git a/src/OpenSEE/Scripts/TSX/store/analyticSlice.tsx b/src/OpenSEE/Scripts/TSX/store/analyticSlice.tsx deleted file mode 100644 index 1eb3d56f..00000000 --- a/src/OpenSEE/Scripts/TSX/store/analyticSlice.tsx +++ /dev/null @@ -1,117 +0,0 @@ -//****************************************************************************************************** -// analyticSlice.tsx - 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 11/23/2020 - C. Lackner -// Generated original version of source code. -// -//****************************************************************************************************** -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { createSelector } from 'reselect'; -import { OpenSee } from '../global'; -import { UpdateAnalyticPlot } from './dataSlice'; -import { RootState } from './store'; - -// #region [ Thunks ] -export const UpdateAnalytic = createAsyncThunk('Analytic/updateAnalytic', async (arg: { settings: OpenSee.IAnalyticStore, key?: OpenSee.IGraphProps }, thunkAPI) => { - if(arg.key) - thunkAPI.dispatch(UpdateAnalyticPlot({ key: arg.key })); - return Promise.resolve(); -}) -// #endregion - -export const AnalyticReducer = createSlice({ - name: 'Analytic', - initialState: { - Harmonic: 1, - LPFOrder: 2, - HPFOrder: 2, - Trc: 500, - FFTCycles: 1, - FFTStartTime: 0 - } as OpenSee.IAnalyticStore, - reducers: {}, - extraReducers: (builder) => { - builder.addCase(UpdateAnalytic.pending, (state, action) => { - if (action.meta.arg.settings.Harmonic !== undefined) - state.Harmonic = action.meta.arg.settings.Harmonic; - if (action.meta.arg.settings.HPFOrder !== undefined) - state.HPFOrder = action.meta.arg.settings.HPFOrder; - if (action.meta.arg.settings.LPFOrder !== undefined) - state.LPFOrder = action.meta.arg.settings.LPFOrder; - if (action.meta.arg.settings.Trc !== undefined) - state.Trc = action.meta.arg.settings.Trc; - if (action.meta.arg.settings.FFTCycles !== undefined) - state.FFTCycles = action.meta.arg.settings.FFTCycles; - if (action.meta.arg.settings.FFTStartTime !== undefined) - state.FFTStartTime = action.meta.arg.settings.FFTStartTime; - }); - } -}); - -export const {} = AnalyticReducer.actions; -export default AnalyticReducer.reducer; - -// #endregion - -// #region [ Selectors ] -export const SelectHarmonic = (state: RootState) => state.Analytic.Harmonic; -export const SelectTRC = (state: RootState) => state.Analytic.Trc; -export const SelectLPF = (state: RootState) => state.Analytic.LPFOrder; -export const SelectHPF = (state: RootState) => state.Analytic.HPFOrder; -export const SelectCycles = (state: RootState) => state.Analytic.FFTCycles; -export const SelectAnalytics = (state: RootState) => state.Analytic; - -export const SelectShowFFTWindow = (state: RootState) => { - const fftPlot = state.Data.Plots.find(plot => plot.key.DataType === "FFT") - if (fftPlot) - return true - else - return false -} - -export const SelectFFTWindow = createSelector( - (state: RootState) => state.Analytic.FFTCycles, - (state: RootState) => state.Analytic.FFTStartTime, - (cycle, startTime) => ([startTime, startTime + (cycle * 1 / 60.0 * 1000.0)] as [number, number]) -); - -export const SelectAnalyticOptions = (key: OpenSee.graphType) => { - return createSelector( - (state: RootState) => state.Analytic.Harmonic, - (state: RootState) => state.Analytic.LPFOrder, - (state: RootState) => state.Analytic.HPFOrder, - (state: RootState) => state.Analytic.Trc, - (state: RootState) => state.Analytic.FFTStartTime, - (state: RootState) => state.Analytic.FFTCycles, - (harmonic, lpf, hpf, Trc, fftStart, fftCycle) => { - if (key == 'LowPassFilter') - return [lpf]; - if (key == 'HighPassFilter') - return [hpf]; - if (key == 'Harmonic') - return [harmonic]; - if (key == 'Rectifier') - return [Trc]; - if (key == 'FFT') - return [fftCycle, fftStart]; - return []; - }); -} - -// #endregion - diff --git a/src/OpenSEE/Scripts/TSX/store/dataSlice.tsx b/src/OpenSEE/Scripts/TSX/store/dataSlice.tsx deleted file mode 100644 index d98b12be..00000000 --- a/src/OpenSEE/Scripts/TSX/store/dataSlice.tsx +++ /dev/null @@ -1,1617 +0,0 @@ -//****************************************************************************************************** -// dataSlice.tsx - 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 11/01/2020 - C. Lackner -// Generated original version of source code. -// -//****************************************************************************************************** -import { createSlice, createAsyncThunk, createSelector, PayloadAction } from '@reduxjs/toolkit'; -import { OpenSee } from '../global'; -import * as _ from 'lodash'; -import { plotTypes } from './settingSlice'; -import { AddRequest, AppendRequest, CancelAnalytics } from './RequestHandler'; -import { emptygraph, getData, getDetailedData } from './GraphLogic'; -import { RootState } from './store'; -import { defaultSettings } from '../defaults'; -import { sortGraph } from '../Graphs/Utilities' - -// #region [ Thunks ] -//Thunk to Get Detailed Data -export const InitiateDetailed = createAsyncThunk('Data/InitiateDetailed', async (arg: OpenSee.IGraphProps, thunkAPI) => { - AppendRequest(arg, getDetailedData(arg, (thunkAPI.getState() as OpenSee.IRootState).Analytic, (key, data) => { - thunkAPI.dispatch(DataReducer.actions.ReplaceData({ key, data })) - })) - return Promise.resolve(); -}); - - -// Thunk To Add New Plot -export const AddPlot = createAsyncThunk('Data/addPlot', async (arg: { key: OpenSee.IGraphProps, yLimits?: OpenSee.IUnitCollection, isZoomed?: boolean, fftLimits?: [number, number], cycleLimits?: [number, number] }, thunkAPI) => { - let plot = (thunkAPI.getState() as RootState).Data.Plots.find(item => item.key.DataType == arg.key.DataType && item.key.EventId == arg.key.EventId) - const state = (thunkAPI.getState() as OpenSee.IRootState) - const singlePlot = state.Settings.SinglePlot - - if (plot === null || plot.loading !== 'Loading') - return Promise.resolve(); - - // Adding Data to the Plot - let analyticOptions = (thunkAPI.getState() as OpenSee.IRootState).Analytic; - - let handles = getData(arg.key, analyticOptions, async data => { - await thunkAPI.dispatch(DataReducer.actions.AppendData({ key: arg.key, data, defaultTraces: state.Settings.DefaultTrace, defaultV: state.Settings.DefaultVType, eventID: arg.key.EventId })); - - const updatedState = (thunkAPI.getState() as OpenSee.IRootState); - const updatedPlot = updatedState.Data.Plots.find(item => item.key.DataType === arg.key.DataType && item.key.EventId === arg.key.EventId); - const singleOverlappingPlot = updatedState.Data.Plots.find(item => item.key.DataType === arg.key.DataType && item.key.EventId === -1) - - - //Only dispatch to the overlapping single plot after the first call to AppendData finishes and if it exists - if (singlePlot) { - if (singleOverlappingPlot) - thunkAPI.dispatch(DataReducer.actions.AppendData({ key: { EventId: -1, DataType: arg.key.DataType }, data: _.cloneDeep(updatedPlot.data), defaultTraces: updatedState.Settings.DefaultTrace, defaultV: updatedState.Settings.DefaultVType, eventID: arg.key.EventId })); - else - thunkAPI.dispatch(AddSingleOverlappingPlot(arg.key)); - } - }, - () => { - thunkAPI.dispatch(InitiateDetailed(arg.key)) - } - ); - - AddRequest(arg.key, handles); - return await Promise.all(handles); -}) - -// Thunk To Remove Plot -export const RemovePlot = createAsyncThunk('Data/removePlot', async (arg: OpenSee.IGraphProps, thunkAPI) => { - const state = (thunkAPI.getState() as OpenSee.IRootState) - const singlePlot = state.Settings.SinglePlot - const plotIndex = state.Data.Plots.findIndex(item => item.key.DataType == arg.DataType && item.key.EventId == arg.EventId) - const plotData = state.Data.Plots[plotIndex].data - - if (plotIndex > -1) { - thunkAPI.dispatch(DataReducer.actions.RemovePlot(plotIndex)) - - //Remove data from the overlapping single plot if enabled - if (singlePlot) - thunkAPI.dispatch(DataReducer.actions.RemoveOverlappingData({ key: arg, data: _.cloneDeep(plotData) })) - } - - return await Promise.resolve(); -}) - -// Thunk To Add New Single Overlapping Plot -export const AddSingleOverlappingPlot = createAsyncThunk('Data/addOverlappingPlot', async (arg: OpenSee.IGraphProps, thunkAPI) => { - const state = (thunkAPI.getState() as OpenSee.IRootState) - const singleOverlappingPlot = state.Data.Plots.find(plot => plot.key.DataType === arg.DataType && plot.key.EventId === -1) - const currentPlot = state.Data.Plots.find(plot => plot.key.DataType === arg.DataType && plot.key.EventId === arg.EventId) - - if (singleOverlappingPlot === null || singleOverlappingPlot.loading !== 'Loading') - return Promise.resolve(); - - // Adding Data with matching datatypes - thunkAPI.dispatch(DataReducer.actions.AppendData({ key: { EventId: -1, DataType: currentPlot.key.DataType }, data: _.cloneDeep(currentPlot.data), defaultTraces: state.Settings.DefaultTrace, defaultV: state.Settings.DefaultVType, eventID: currentPlot.key.EventId })); //not really sure what requestID and secondary is... - - return await Promise.resolve(); -}) - -// Thunk To Refetch Data for Analytic Plots -export const UpdateAnalyticPlot = createAsyncThunk('Data/updateAnalyticPlot', async (arg: { key: OpenSee.IGraphProps }, thunkAPI) => { - let plot = (thunkAPI.getState() as RootState).Data.Plots.find(item => item.key.DataType == arg.key.DataType && item.key.EventId == arg.key.EventId) - const state = (thunkAPI.getState() as OpenSee.IRootState) - - if (plot === null) - return Promise.resolve(); - - - thunkAPI.dispatch(DataReducer.actions.RemoveData(arg.key)) - - // Adding Data to the Plot - let analyticOptions = (thunkAPI.getState() as OpenSee.IRootState).Analytic; - let handles = getData(arg.key, analyticOptions, data => { - thunkAPI.dispatch(DataReducer.actions.AppendData({ key: arg.key, data, defaultTraces: state.Settings.DefaultTrace, defaultV: state.Settings.DefaultVType, eventID: plot.key.EventId })); - }, () => { - thunkAPI.dispatch(InitiateDetailed(arg.key)) - }); - - AddRequest(arg.key, handles); - return await Promise.all(handles); -}) - - -//Thunk to update Time Limits -export const SetTimeLimit = createAsyncThunk('Data/setTimeLimit', (arg: { start: number, end: number }, thunkAPI) => { - thunkAPI.dispatch(DataReducer.actions.UpdateTimeLimit({ ...arg })) - return Promise.resolve(); -}) - -//Thunk to update Cycle Limits -export const SetCycleLimit = createAsyncThunk('Data/SetCycleLimit', (arg: { start: number, end: number }, thunkAPI) => { - thunkAPI.dispatch(DataReducer.actions.UpdateCycleLimits({ ...arg })) - return Promise.resolve(); -}) - - -//Thunk to update FFT Limits -export const SetFFTLimits = createAsyncThunk('Data/SetFFTLimits', (arg: { start: number, end: number }, thunkAPI) => { - thunkAPI.dispatch(DataReducer.actions.UpdateFFTLimits({ ...arg })) - return Promise.resolve(); -}) - - -//Thunk to Enable or Disable Trace -export const EnableTrace = createAsyncThunk('Data/EnableTrace', (arg: { key: OpenSee.IGraphProps, trace: number[], enabled: boolean }, thunkAPI) => { - thunkAPI.dispatch(DataReducer.actions.UpdateTrace({ ...arg })) - return Promise.resolve(); -}); - - -//Thunk to Reset Zoom -export const ResetZoom = createAsyncThunk('Data/Reset', (arg: { start: number, end: number }, thunkAPI) => { - thunkAPI.dispatch(DataReducer.actions.UpdateTimeLimit({ ...arg })); - - // FFT Limits get updated base on values not eventTime - let state = (thunkAPI.getState() as OpenSee.IRootState); - let fftPlot = state.Data.Plots.find(item => item.key.DataType == 'FFT'); - let overlappingWaveform = state.Data.Plots.find(item => item.key.DataType == 'OverlappingWave'); - - if (fftPlot) { - let start = Math.min(...fftPlot.data.map(item => Math.min(...item.DataPoints.map(pt => pt[0])))); - let end = Math.max(...fftPlot.data.map(item => Math.max(...item.DataPoints.map(pt => pt[0])))); - thunkAPI.dispatch(SetFFTLimits({ start: start, end: end })); - } - - if (overlappingWaveform) { - let start = Math.min(...overlappingWaveform.data.map(item => Math.min(...item.DataPoints.map(pt => pt[0]).filter(val => !isNaN(val))))); - let end = Math.max(...overlappingWaveform.data.map(item => Math.max(...item.DataPoints.map(pt => pt[0]).filter(val => !isNaN(val))))); - thunkAPI.dispatch(SetCycleLimit({ start: start, end: end })); - } - - - thunkAPI.dispatch(DataReducer.actions.ResetZoom()); - - return Promise.resolve(); -}) - - -// Thunk to Set Zoomed YLimits -export const SetZoomedLimits = createAsyncThunk('Data/SetZoomedLimits', (arg: { limits: [number, number], key: OpenSee.IGraphProps }, thunkAPI) => { - const state = (thunkAPI.getState() as OpenSee.IRootState); - const plot = state.Data.Plots.find(plot => plot.key.DataType == arg.key.DataType && plot.key.EventId == arg.key.EventId); - const primaryAxis = getPrimaryAxis(plot.key) - let oldLimits: [number, number] = [0, 1] - - if (plot.yLimits[primaryAxis].isManual) - oldLimits = plot.yLimits[primaryAxis].manualLimits - else if (plot.isZoomed) - oldLimits = plot.yLimits[primaryAxis].zoomedLimits - else - oldLimits = plot.yLimits[primaryAxis].dataLimits - - - thunkAPI.dispatch(DataReducer.actions.SetZoomedLimits({ oldLimits: oldLimits, key: arg.key, newLimits: arg.limits })); - return Promise.resolve(); -}) - -export const SetUnit = createAsyncThunk('Data/SetUnit', (arg: { unit: OpenSee.Unit, value: number, auto: boolean, key: OpenSee.IGraphProps }, thunkAPI) => { - thunkAPI.dispatch(UpdateActiveUnits({ unit: arg.unit, value: arg.value, auto: arg.auto, key: arg.key })); -}) - - -// #endregion - -export const DataReducer = createSlice({ - name: 'Data', - initialState: { - startTime: 0 as number, - endTime: 0 as number, - Plots: [] as OpenSee.IGraphstate[], - fftLimits: [0, 0], - cycleLimit: [0, 1000.0 / 60.0], - } as OpenSee.IDataState, - reducers: { - RemovePlot: (state: OpenSee.IDataState, action: PayloadAction) => { - state.Plots.splice(action.payload, 1); - }, - RemoveData: (state: OpenSee.IDataState, action: PayloadAction) => { - const plot = state.Plots.find(item => item.key.DataType == action.payload.DataType && item.key.EventId == action.payload.EventId) - if (plot) - plot.data = []; - }, - RemoveOverlappingData: (state: OpenSee.IDataState, action: PayloadAction<{ key: OpenSee.IGraphProps, data: OpenSee.iD3DataSeries[] }>) => { - const plot = state.Plots.find(item => item.key.DataType == action.payload.key.DataType && item.key.EventId == -1) - if (plot) { - plot.data = plot.data.filter(data => data.EventID !== action.payload.key.EventId) - if (plot.data.length !== 0) - updateAutoLimits(plot, state.startTime, state.endTime); - else { - const plotIndex = state.Plots.findIndex(item => item.key.DataType == action.payload.key.DataType && item.key.EventId == -1) - state.Plots.splice(plotIndex, 1); - } - } - }, - UpdateActiveUnits: (state: OpenSee.IDataState, action: PayloadAction<{ unit: OpenSee.Unit, value: number, auto: boolean, key: OpenSee.IGraphProps }>) => { - - state.Plots.filter(plot => plot.key.DataType === action.payload.key.DataType).forEach(curPlot => { - - const oldUnitIndex = curPlot.yLimits[action.payload.unit].current - let newUnitIndex = action.payload.value - const oldFactor = defaultSettings.Units[action.payload.unit].options[oldUnitIndex].factor - const newFactor = defaultSettings.Units[action.payload.unit].options[newUnitIndex].factor - const isPU = oldFactor === undefined || newFactor === undefined ? true : false - - curPlot.yLimits[action.payload.unit].isAuto = action.payload.auto - curPlot.yLimits[action.payload.unit].current = action.payload.value - - const axisSetting: OpenSee.IAxisSettings = curPlot.yLimits[action.payload.unit]; - const oldLimits = axisSetting.dataLimits - const filteredData = curPlot.data.filter(item => item.Enabled && item.Unit === action.payload.unit); - - //handle autoUnit case - let unitIndex = updateActiveUnits(curPlot.yLimits, action.payload.unit, filteredData, state.startTime, state.endTime, null); - if (unitIndex) { - curPlot.yLimits[action.payload.unit].current = unitIndex - newUnitIndex = unitIndex - } - - if (curPlot.key.DataType != 'FFT' && curPlot.key.DataType != 'OverlappingWave') { - const limits = recomputeDataLimits(state.startTime, state.endTime, filteredData, curPlot.yLimits[action.payload.unit].current); - axisSetting.dataLimits = limits; - if (isPU) { - axisSetting.zoomedLimits = scaleLimits(oldLimits, limits, axisSetting.zoomedLimits) - axisSetting.manualLimits = scaleLimits(oldLimits, limits, axisSetting.manualLimits) - } - else { - axisSetting.manualLimits = scaleLimitsByFactor(oldUnitIndex, newUnitIndex, action.payload.unit, axisSetting.manualLimits) - axisSetting.zoomedLimits = scaleLimitsByFactor(oldUnitIndex, newUnitIndex, action.payload.unit, axisSetting.zoomedLimits) - } - } - else if (curPlot.key.DataType == 'FFT') { - const limits = recomputeDataLimits(state.fftLimits[0], state.fftLimits[1], filteredData, curPlot.yLimits[action.payload.unit].current) - axisSetting.dataLimits = limits; - if (isPU) { - axisSetting.zoomedLimits = scaleLimits(oldLimits, limits, axisSetting.zoomedLimits) - axisSetting.manualLimits = scaleLimits(oldLimits, limits, axisSetting.manualLimits) - } - else { - axisSetting.manualLimits = scaleLimitsByFactor(oldUnitIndex, newUnitIndex, action.payload.unit, axisSetting.manualLimits) - axisSetting.zoomedLimits = scaleLimitsByFactor(oldUnitIndex, newUnitIndex, action.payload.unit, axisSetting.zoomedLimits) - } - } - else if (curPlot.key.DataType == 'OverlappingWave') { - const limits = recomputeDataLimits(state.cycleLimit[0], state.cycleLimit[1], filteredData, curPlot.yLimits[action.payload.unit].current); - axisSetting.dataLimits = limits; - if (isPU) { - axisSetting.zoomedLimits = scaleLimits(oldLimits, limits, axisSetting.zoomedLimits) - axisSetting.manualLimits = scaleLimits(oldLimits, limits, axisSetting.manualLimits) - } - else { - axisSetting.manualLimits = scaleLimitsByFactor(oldUnitIndex, newUnitIndex, action.payload.unit, axisSetting.manualLimits) - axisSetting.zoomedLimits = scaleLimitsByFactor(oldUnitIndex, newUnitIndex, action.payload.unit, axisSetting.zoomedLimits) - } - } - - }) - saveSettings(state); - - return state; - }, - SetIsManual: (state: OpenSee.IDataState, action: PayloadAction<{ key: OpenSee.IGraphProps, unit: OpenSee.Unit, manual: boolean }>) => { - let plot = state.Plots.find(plot => plot.key.DataType === action.payload.key.DataType && plot.key.EventId === action.payload.key.EventId); - plot.yLimits[action.payload.unit].isManual = action.payload.manual - - const isValidNumber = (value) => !isNaN(value) && isFinite(value); - - const invalidZoomedLimits = (plot.yLimits[action.payload.unit].zoomedLimits === null) || !isValidNumber(plot.yLimits[action.payload.unit].zoomedLimits[0]) || !isValidNumber(plot.yLimits[action.payload.unit].zoomedLimits[1]); - const invalidDataLimits = (plot.yLimits[action.payload.unit].dataLimits === null) || !isValidNumber(plot.yLimits[action.payload.unit].dataLimits[0]) || !isValidNumber(plot.yLimits[action.payload.unit].dataLimits[1]); - - if (plot.isZoomed && !invalidZoomedLimits) - plot.yLimits[action.payload.unit].manualLimits = plot.yLimits[action.payload.unit].zoomedLimits - else if (!invalidDataLimits) - plot.yLimits[action.payload.unit].manualLimits = plot.yLimits[action.payload.unit].dataLimits - - return state; - }, - - UpdateTimeLimit: (state: OpenSee.IDataState, action: PayloadAction<{ start: number, end: number }>) => { - if (Math.abs(action.payload.start - action.payload.end) < 10) - return state; - - state.startTime = action.payload.start; - state.endTime = action.payload.end; - - state.Plots.forEach(graph => { - if (graph.key.DataType === "FFT") - updateAutoLimits(graph, state.fftLimits[0], state.fftLimits[1]); - else if (graph.key.DataType === "OverlappingWave") - updateAutoLimits(graph, state.cycleLimit[0], state.cycleLimit[1]); - else - updateAutoLimits(graph, state.startTime, state.endTime); - - }); - - return state; - }, - AppendData: (state: OpenSee.IDataState, action: PayloadAction<{ - key: OpenSee.IGraphProps, data: Array, - defaultTraces: OpenSee.IDefaultTrace, defaultV: "L-L" | "L-N", - eventID: number - }>) => { - let currentPlot = state.Plots.find(item => item.key.DataType == action.payload.key.DataType && item.key.EventId == action.payload.key.EventId) - - if (currentPlot) { - const orignalLength = currentPlot.data.length - //update plot with unit settings from local storage - applyLocalSettings(currentPlot) - - currentPlot.data.push(...action.payload.data); - const newLength = currentPlot.data.length - - let extendEnabled = GetDefaults(action.payload.key.DataType, action.payload.defaultTraces, action.payload.defaultV, currentPlot.data); - - for (let i = orignalLength; i < newLength; i++) { - currentPlot.data[i].EventID = action.payload.eventID - } - - for (let i = 0; i < newLength; i++) { - currentPlot.data[i].Enabled = extendEnabled[i]; - } - - const RelevantAxises = _.uniq(currentPlot.data.map(s => s.Unit)); - - RelevantAxises.forEach(axis => { - let filteredData = currentPlot.data.filter(item => item.Unit === axis && item.Enabled); - let index = updateActiveUnits(currentPlot.yLimits, axis, filteredData, state.startTime, state.endTime, null); - if (index) - currentPlot.yLimits[axis].current = index; - }) - - if (currentPlot.key.DataType === 'FFT') { - state.fftLimits = [Math.min(...currentPlot.data.map(item => Math.min(...item.DataPoints.map(pt => pt[0])))), Math.max(...currentPlot.data.map(item => Math.max(...item.DataPoints.map(pt => pt[0]))))] - updateAutoLimits(currentPlot, state.fftLimits[0], state.fftLimits[1]); - } else if (currentPlot.key.DataType === 'OverlappingWave') - updateAutoLimits(currentPlot, state.cycleLimit[0], state.cycleLimit[1]); - else - updateAutoLimits(currentPlot, state.startTime, state.endTime); - } - - return state - - }, - UpdateFFTLimits: (state: OpenSee.IDataState, action: PayloadAction<{ start: number, end: number }>) => { - if (Math.abs(action.payload.start - action.payload.end) < 1) - return state; - - state.fftLimits[0] = action.payload.start - state.fftLimits[1] = action.payload.end - - const fftPlot = state.Plots.find(plot => plot.key.DataType === "FFT") - updateAutoLimits(fftPlot, state.fftLimits[0], state.fftLimits[1]); - return state; - }, - UpdateCycleLimits: (state: OpenSee.IDataState, action: PayloadAction<{ start: number, end: number }>) => { - if (Math.abs(action.payload.start - action.payload.end) < 5) - return state; - - state.cycleLimit[0] = action.payload.start - state.cycleLimit[1] = action.payload.end - - const plot = state.Plots.find(plot => plot.key.DataType === "OverlappingWave") - updateAutoLimits(plot, state.cycleLimit[0], state.cycleLimit[1]); - - return state; - - }, - UpdateTrace: (state: OpenSee.IDataState, action: PayloadAction<{ key: OpenSee.IGraphProps, trace: number[], enabled: boolean }>) => { - // Find the index of the plot in the state - let curPlot = state.Plots.find(plot => plot.key.DataType == action.payload.key.DataType && plot.key.EventId == action.payload.key.EventId); - if (!curPlot) - return; - - // Update only the selected plot - action.payload.trace.forEach(traceIndex => { - if (traceIndex < curPlot.data.length) { - curPlot.data[traceIndex].Enabled = action.payload.enabled; - } - }); - - // Recompute limits and update units - const relevantTraces = action.payload.trace.map(index => curPlot.data[index]) - const RelevantAxis = _.uniq(relevantTraces.map(s => s?.Unit)); - - if (RelevantAxis.length > 0) - RelevantAxis.forEach(axis => { - if (axis === undefined) - return - const axisSetting = curPlot.yLimits[axis]; - const relevantData = curPlot.data.filter(item => item.Enabled && item.Unit === axis); - - if (curPlot.key.DataType !== 'FFT' && curPlot.key.DataType !== 'OverlappingWave') { - let recomputedLimits = recomputeDataLimits(state.startTime, state.endTime, relevantData, curPlot.yLimits[axis].current) - axisSetting.dataLimits = recomputedLimits; - axisSetting.zoomedLimits = recomputeNonAutoLimits(curPlot.yLimits[getPrimaryAxis(action.payload.key)].dataLimits, curPlot.yLimits[getPrimaryAxis(action.payload.key)].zoomedLimits, recomputedLimits) - } else if (curPlot.key.DataType === 'FFT') { - const recomputedLimits = recomputeDataLimits(state.fftLimits[0], state.fftLimits[1], relevantData, curPlot.yLimits[axis].current); - axisSetting.dataLimits = recomputedLimits; - axisSetting.zoomedLimits = recomputeNonAutoLimits(curPlot.yLimits[getPrimaryAxis(action.payload.key)].dataLimits, curPlot.yLimits[getPrimaryAxis(action.payload.key)].zoomedLimits, recomputedLimits) - } else if (curPlot.key.DataType === 'OverlappingWave') { - const recomputedLimits = recomputeDataLimits(state.cycleLimit[0], state.cycleLimit[1], relevantData, curPlot.yLimits[axis].current); - axisSetting.dataLimits = recomputedLimits; - axisSetting.zoomedLimits = recomputeNonAutoLimits(curPlot.yLimits[getPrimaryAxis(action.payload.key)].dataLimits, curPlot.yLimits[getPrimaryAxis(action.payload.key)].zoomedLimits, recomputedLimits) - - } - updateActiveUnits(curPlot.yLimits, axis, relevantData, state.startTime, state.endTime, null); - }); - }, - SetSelectPoint: (state: OpenSee.IDataState, action: PayloadAction<{ time: number, key: OpenSee.IGraphProps }>) => { - state.Plots.forEach(plot => { - let shortestDataObject = _.minBy(plot.data, dataObject => dataObject.DataPoints.length); - - if (plot?.data?.length > 0) { - let dataIndex = getIndex(action.payload.time, shortestDataObject.DataPoints) - plot.selectedIndixes.push(dataIndex); - } - }) - }, - ClearSelectPoints: (state: OpenSee.IDataState) => { - state.Plots.forEach(plot => plot.selectedIndixes = []); - }, - RemoveSelectPoints: (state: OpenSee.IDataState, action: PayloadAction) => { - state.Plots.forEach(plot => plot.selectedIndixes.splice(action.payload, 1)); - }, - SetZoomedLimits: (state: OpenSee.IDataState, action: PayloadAction<{ oldLimits: [number, number], key: OpenSee.IGraphProps, newLimits: [number, number] }>) => { - const curPlot = state.Plots.find(plot => plot.key.DataType == action.payload.key.DataType && plot.key.EventId == action.payload.key.EventId); - if (curPlot) { - const RelevantAxis = _.uniq(curPlot.data.filter(item => item.Enabled).map(s => s.Unit)); - RelevantAxis.forEach(axis => { - if (axis === getPrimaryAxis(action.payload.key)) - curPlot.yLimits[axis].zoomedLimits = action.payload.newLimits - else if (curPlot.yLimits[axis].isManual) - curPlot.yLimits[axis].zoomedLimits = recomputeNonAutoLimits(action.payload.oldLimits, action.payload.newLimits, curPlot.yLimits[axis].manualLimits); - else if (curPlot.isZoomed) - curPlot.yLimits[axis].zoomedLimits = recomputeNonAutoLimits(action.payload.oldLimits, action.payload.newLimits, curPlot.yLimits[axis].zoomedLimits); - else - curPlot.yLimits[axis].zoomedLimits = recomputeNonAutoLimits(action.payload.oldLimits, action.payload.newLimits, curPlot.yLimits[axis].dataLimits); - }) - - - curPlot.isZoomed = true; - } - }, - SetManualLimits: (state: OpenSee.IDataState, action: PayloadAction<{ - limits: [number, number], - key: OpenSee.IGraphProps, - axis: OpenSee.Unit, - auto: boolean - factor?: number - }>) => { - const curPlot = state.Plots.find(plot => plot.key.DataType == action.payload.key.DataType && plot.key.EventId == action.payload.key.EventId); - if (curPlot) { - curPlot.yLimits[action.payload.axis].isManual = true; - - if (curPlot.isZoomed) //cover case of user zooming first then manually editing those.. - curPlot.yLimits[action.payload.axis].zoomedLimits = action.payload.limits; - - curPlot.yLimits[action.payload.axis].manualLimits = action.payload.limits; - - if (action.payload.auto) { - let revelantData = curPlot.data.filter(data => data.Enabled && data.Unit === action.payload.axis) - - let index = updateActiveUnits(curPlot.yLimits, action.payload.axis, revelantData, state.startTime, state.endTime, action.payload.limits); - if (index) { - curPlot.yLimits[action.payload.axis] = index; - let newManualLimits = [action.payload.limits[0] * action.payload.factor, action.payload.limits[1] * action.payload.factor] - curPlot.yLimits[action.payload.axis].manualLimits = newManualLimits; - - } - } - - } - }, - ResetZoom: (state: OpenSee.IDataState) => { - state.Plots.forEach(plot => { - plot.isZoomed = false; - const RelevantAxis = _.uniq(plot.data.map(s => s.Unit)); - RelevantAxis.forEach(axis => { - plot.yLimits[axis].zoomedLimits = [0, 1]; - }) - - }) - }, - ReplaceData: (state, action: PayloadAction<{ key: OpenSee.IGraphProps, data: Array }>) => { - let plot = state.Plots.find(plot => plot.key.EventId === action.payload.key.EventId && plot.key.DataType === action.payload.key.DataType) - if (plot) { - let updated = []; - - if (action.payload.data && action.payload.data?.length > 0) { - - action.payload.data.forEach(d => { - let dIndex = plot.data.findIndex((od, di) => od.LegendGroup == d.LegendGroup && od.LegendHorizontal == d.LegendHorizontal && od.LegendVertical == d.LegendVertical && od.LegendVGroup == d.LegendVGroup && updated.indexOf(di) == -1); - const data = plot.data.find((od, di) => od.LegendGroup == d.LegendGroup && od.LegendHorizontal == d.LegendHorizontal && od.LegendVertical == d.LegendVertical && od.LegendVGroup == d.LegendVGroup && updated.indexOf(di) == -1); - if (dIndex !== -1) { - let detailedData = d; - detailedData.Enabled = data.Enabled; - detailedData.EventID = data.EventID; - updated.push(dIndex); - plot.data[dIndex] = d; - } - }); - } - return state; - } - }, - }, - extraReducers: (builder) => { - builder.addCase(AddPlot.pending, (state, action) => { - let plot = state.Plots.find(item => item.key.DataType == action.meta.arg.key.DataType && item.key.EventId == action.meta.arg.key.EventId); - - if (plot === undefined) { - plot = _.cloneDeep(emptygraph); - plot.loading = 'Loading'; - state.Plots.push(plot) - } - - if (action.meta.arg.yLimits) - Object.keys(action.meta.arg.yLimits).forEach(unit => { - plot.yLimits[unit] = action.meta.arg.yLimits[unit] - }) - - if (action.meta.arg.isZoomed !== undefined) - plot.isZoomed = action.meta.arg.isZoomed - - plot.key = action.meta.arg.key; - - const singlePlot = state.Plots.find(plot => plot.key.EventId === -1 && plot.key.DataType === action.meta.arg.key.DataType) - if (singlePlot) - singlePlot.loading = 'Loading' - - return state - }); - builder.addCase(AddPlot.fulfilled, (state, action) => { - let plot = state.Plots.find(item => item.key.DataType == action.meta.arg.key.DataType && item.key.EventId == action.meta.arg.key.EventId); - if (plot === undefined) - return state - - plot.loading = 'Idle' - - const singlePlot = state.Plots.find(plot => plot.key.EventId === -1 && plot.key.DataType === action.meta.arg.key.DataType) - if (singlePlot) { - const evtIDs = _.uniq(state.Plots.filter(plot => plot.data.length > 1).map(plot => plot.key.EventId).filter(id => id !== -1)) - const evtIDsPresent = _.uniq(singlePlot.data.map(data => data.EventID)) - const allDataPresent = evtIDs.every(id => { - return evtIDsPresent.includes(id) - }) - - if (allDataPresent) - singlePlot.loading = 'Idle'; - } - - - if (action.meta.arg.fftLimits) - state.fftLimits = action.meta.arg.fftLimits - if (action.meta.arg.fftLimits) - state.cycleLimit = action.meta.arg.cycleLimits - - - return state - }); - builder.addCase(AddPlot.rejected, (state, action) => { - let plot = state.Plots.find(item => item.key.DataType == action.meta.arg.key.DataType && item.key.EventId == action.meta.arg.key.EventId); - if (plot === undefined) - return state - - plot.loading = 'Error' - - return state - }); - - builder.addCase(UpdateAnalyticPlot.pending, (state, action) => { - let plot = state.Plots.find(item => item.key.DataType == action.meta.arg.key.DataType && item.key.EventId == action.meta.arg.key.EventId); - if (plot) - plot.loading = 'Loading'; - - return state - }); - builder.addCase(UpdateAnalyticPlot.fulfilled, (state, action) => { - let plot = state.Plots.find(item => item.key.DataType == action.meta.arg.key.DataType && item.key.EventId == action.meta.arg.key.EventId); - if (plot) - plot.loading = 'Idle'; - - return state - }); - builder.addCase(AddSingleOverlappingPlot.pending, (state, action) => { - let plot = state.Plots.find(item => item.key.DataType == action.meta.arg.DataType && item.key.EventId == -1); - - if (plot === undefined) { - plot = _.cloneDeep(emptygraph); - state.Plots.push(plot) - } - - plot.key = { EventId: -1, DataType: action.meta.arg.DataType }; - plot.loading = 'Loading'; - - - return state - }); - builder.addCase(AddSingleOverlappingPlot.fulfilled, (state, action) => { - let plot = state.Plots.find(item => item.key.DataType == action.meta.arg.DataType && item.key.EventId == -1); - if (plot) { - const evtIDs = _.uniq(state.Plots.filter(plot => plot.data.length > 1).map(plot => plot.key.EventId).filter(id => id !== -1)) - const evtIDsPresent = _.uniq(plot.data.map(data => data.EventID)) - const allDataPresent = evtIDs.every(id => { - return evtIDsPresent.includes(id) - }) - - if (allDataPresent) - plot.loading = 'Idle'; - } - - return state - }); - } - -}); - - -export const { SetIsManual, SetSelectPoint, RemoveSelectPoints, ClearSelectPoints, UpdateActiveUnits, SetManualLimits, AppendData, ReplaceData } = DataReducer.actions; -export default DataReducer.reducer; - -// #endregion - -// #region [ Individual Selectors ] -export const SelectFFTLimits = (state: OpenSee.IRootState) => state.Data.fftLimits; -export const SelectCycleLimits = (state: OpenSee.IRootState) => state.Data.cycleLimit; -export const SelectStartTime = (state: OpenSee.IRootState) => state.Data.startTime; -export const SelectEndTime = (state: OpenSee.IRootState) => state.Data.endTime; -export const SelectCycleStart = (state: OpenSee.IRootState) => state.Data.cycleLimit[0] -export const SelectCycleEnd = (state: OpenSee.IRootState) => state.Data.cycleLimit[1] - - -export const SelectOverlappingEvents = (graphType: OpenSee.graphType) => createSelector( - (state: RootState) => state.Data.Plots, - (state: RootState) => state.EventInfo.EventID, - (plots, evtID) => { - const filteredPlots = plots.filter(plot => plot.key.EventId !== evtID && plot.key.EventId !== -1 && plot.key.DataType === graphType).map(plot => plot.key) - //order by eventID because we groupBy eventID in openSEE.tsx - const sortedPlots = _.orderBy(filteredPlots, "EventId", "desc") - return sortedPlots; - }) - -export const SelectDisplayed = createSelector( - (state: RootState) => state.Data.Plots, - (plots) => ({ - Voltage: plots.some(p => p.key.DataType == 'Voltage'), - Current: plots.some(p => p.key.DataType == 'Current'), - TripCoil: plots.some(p => p.key.DataType == 'TripCoil'), - Analogs: plots.some(p => p.key.DataType == 'Analogs'), - Digitals: plots.some(p => p.key.DataType == 'Digitals') - }) -) - -export const SelectPlotKeys = createSelector( - (state: RootState) => state.Data.Plots, - (state: RootState) => state.Settings.SinglePlot, - (plots, singlePlot) => { - let keys = plots.map(plot => plot.key) - if (singlePlot) - keys = keys.filter(key => key.EventId === -1) - - keys = _.uniq(keys) - keys.sort(sortGraph) - - return keys?.length > 0 ? keys : [] - } -) - -// Returns a List of keys for Plots that should be displayed. -export const SelectListGraphs = createSelector( - (state: RootState) => state.Data.Plots, - (state: RootState) => state.Settings.SinglePlot, - (plots, singlePlot) => { - let keys = plots.map(p => p.key) - - if (singlePlot) - return _.groupBy(keys.filter(item => item.EventId === -1), "EventId"); - - return _.groupBy(keys.filter(item => item.EventId !== -1), "EventId"); - } -) - -//Returns the DataType of plots that are Analytics -export const SelectAnalytics = createSelector( - (state: RootState) => state.Data.Plots, - (state: RootState) => state.EventInfo.EventID, - (plots, evtID) => { - const analytics = ['FirstDerivative', 'ClippedWaveforms', 'Frequency', 'HighPassFilter', 'LowPassFilter', 'MissingVoltage', 'OverlappingWave', 'Power', 'Impedance', 'Rectifier', 'RapidVoltage', 'RemoveCurrent', 'Harmonic', 'SymetricComp', 'THD', 'Unbalance', 'FaultDistance', 'Restrike', 'I2T'] as OpenSee.graphType[]; - let plotTypes = plots.filter(plot => plot.key.EventId === evtID && analytics.includes(plot.key.DataType)).map(plot => plot.key.DataType) - - plotTypes = _.uniq(plotTypes) - - if (plotTypes) - return plotTypes - else - return [] - } -) - -export const SelectData = (key: OpenSee.IGraphProps) => createSelector( - (state: OpenSee.IRootState) => state.Data.Plots, - (state: OpenSee.IRootState) => state.Settings.SinglePlot, - (plots, singlePlot) => { - let plot = plots.find(item => item.key.DataType === key.DataType && item.key.EventId === key.EventId); - let overlappingPlot = plots.find(item => item.key.DataType === key.DataType && item.key.EventId === -1) - if (singlePlot) - return overlappingPlot ? overlappingPlot.data : null - return plot ? plot.data : null; - } -); - - -export const SelectEnabled = (key: OpenSee.IGraphProps) => - createSelector( - (state: OpenSee.IRootState) => state.Data.Plots, - (plots) => { - let plot = plots.find(item => item.key.DataType === key.DataType && item.key.EventId === key.EventId); - - if (plot) - return plot.data.map(item => item.Enabled) - else - return [] - } - ); - - -export const SelectRelevantUnits = (key: OpenSee.IGraphProps) => createSelector( - (state: OpenSee.IRootState) => state.Data.Plots, - (Plots) => { - let units: OpenSee.Unit[] = []; - - // Filter relevant plots and collect units - Plots.filter(plot => key.DataType === plot.key.DataType && key.EventId === plot.key.EventId).forEach(plot => { - plot.data.forEach(data => { - if (data.Unit) { - units.push(data.Unit); - } - }); - }); - - //Make sure the primaryAxis is at the beginning of the array for plotting purposes.. - if (units.includes(getPrimaryAxis(key))) { - units = units.filter(unit => unit !== getPrimaryAxis(key)) - units.unshift(getPrimaryAxis(key)) - } - return _.uniq(units); - } -); - - -export const SelectEnabledUnits = (key: OpenSee.IGraphProps) => createSelector( - (state: OpenSee.IRootState) => state.Data.Plots, - (Plots) => { - let units: OpenSee.Unit[] = []; - const plot = Plots.find(plot => key.DataType === plot.key.DataType && key.EventId === plot.key.EventId) - // Filter relevant plots and collect units - if (plot) { - plot.data.forEach(data => { - if (data.Unit && data.Enabled) { - units.push(data.Unit); - } - }); - - //Make sure the primaryAxis is at the beginning of the array - if (units.includes(getPrimaryAxis(key))) { - units = units.filter(unit => unit !== getPrimaryAxis(key)) - units.unshift(getPrimaryAxis(key)) - } - return _.uniq(units); - } - else { - return [] - } - - } -); - - -export const SelectYLimits = (key: OpenSee.IGraphProps) => { - return createSelector( - (state: OpenSee.IRootState) => state.Data, - (data: OpenSee.IDataState) => { - const plot = data.Plots.find(plot => plot.key.EventId === key.EventId && plot.key.DataType === key.DataType) - let result = {} - if (plot) { - Object.keys(plot.yLimits).forEach(unit => { - if (plot.isZoomed) - result[unit] = plot.yLimits[unit].zoomedLimits - else if (plot.yLimits[unit].isManual && plot.yLimits[unit].manualLimits) - result[unit] = plot.yLimits[unit].manualLimits - else - result[unit] = plot.yLimits[unit].dataLimits - }) - } - - return result as OpenSee.IUnitCollection<[number, number]>; - }); -} - -export const SelectOverlappingYLimits = (graphType: OpenSee.graphType) => { - return createSelector( - (state: OpenSee.IRootState) => state.Data.Plots, - (state: OpenSee.IRootState) => state.EventInfo.EventID, - (plots, evtID) => { - let overlappingPlots = plots.filter(plot => plot.key.EventId !== evtID && plot.key.DataType === graphType) - - let result = {}; - if (overlappingPlots.length > 0) { - overlappingPlots.forEach(plot => { - let yLimits = {} - Object.keys(plot.yLimits).forEach(key => { - if (plot.isZoomed) - yLimits[key] = plot.yLimits[key].zoomedLimits; - else if (plot.yLimits[key].isManual && plot.yLimits[key].manualLimits) - yLimits[key] = plot.yLimits[key].manualLimits - else - yLimits[key] = plot.yLimits[key].dataLimits - - }) - result[plot.key.DataType] = yLimits - }) - - return result as OpenSee.IGraphCollection<[number, number]>; - } - - }); -} - -export const SelectLoading = (key: OpenSee.IGraphProps) => { - return (state: OpenSee.IRootState) => { - const plot = state.Data.Plots.find(plot => plot.key.DataType === key.DataType && plot.key.EventId === key.EventId); - if (plot) - return plot.loading - }; -}; - - -export const SelectAutoUnits = (key: OpenSee.IGraphProps) => { - return (state: OpenSee.IRootState) => { - let result = {}; - const plot = state.Data.Plots.find(plot => plot.key.EventId === key.EventId && plot.key.DataType === key.DataType) - if (plot) { - Object.keys(plot.yLimits).forEach(unit => { - result[unit] = plot.yLimits[unit].isAuto - }) - return result; - } - - }; -}; - -export const SelectAxisSettings = (key: OpenSee.IGraphProps) => { - return (state: OpenSee.IRootState) => { - const plot = state.Data.Plots.find(plot => plot.key.DataType === key.DataType && plot.key.EventId === key.EventId); - return plot.yLimits; - }; -}; - - -export const SelectYLabels = (key: OpenSee.IGraphProps) => { - return (state: OpenSee.IRootState) => { - let labels = {} as OpenSee.IUnitCollection - const plot = state.Data.Plots.find(plot => plot.key.DataType === key.DataType && plot.key.EventId === key.EventId); - if (plot) { - Object.keys(plot.yLimits).forEach(unit => { - let short = defaultSettings.Units[unit].options[plot.yLimits[unit].current].short - if (short === undefined) - short = "N/A" - - labels[unit] = `${unit} [${short}]` - }) - return labels; - } - else { - Object.keys(defaultSettings.Units).forEach(unit => { - labels[unit] = "" - }) - return labels; - } - }; -}; - - -export const SelectEventIDs = (state: RootState) => { - let ids = [] - ids.push(state.EventInfo.EventID) - state.OverlappingEvents.EventList.forEach(evt => { - if (evt.Selected) - ids.push(evt.EventID) - }) - - const eventIDS = _.uniq(ids) - return eventIDS -} - -export const SelectFFTEnabled = (state: RootState) => { - const keys = state.Data.Plots.filter(plot => plot.key.DataType === 'FFT') - if (keys?.length > 0) - return true - else - return false -} - -export const SelectIsManual = (key: OpenSee.IGraphProps) => createSelector( - (state: OpenSee.IRootState) => state.Data.Plots, - (plots) => { - let plot = plots.find(p => p.key.DataType === key.DataType && p.key.EventId === key.EventId); - let result = {}; - if (plot) { - Object.keys(plot.yLimits).forEach(key => { - result[key] = plot.yLimits[key].isManual; - }) - return result as OpenSee.IUnitCollection; - } - - } -); - -export const SelectIsOverlappingManual = (graphType: OpenSee.graphType) => createSelector( - (state: OpenSee.IRootState) => state.Data.Plots, - (state: OpenSee.IRootState) => state.EventInfo.EventID, - (plots, evtID) => { - let overlappingPlots = plots.filter(p => p.key.DataType === graphType && p.key.EventId !== evtID); - let result = {}; - if (overlappingPlots.length > 0) { - overlappingPlots.forEach(plot => { - let units = {} - Object.keys(plot.yLimits).forEach(key => { - units[key] = plot.yLimits[key].isManual; - }) - result[plot.key.DataType] = units as OpenSee.IUnitCollection - }) - - return result; - } - - } -); - - -export const SelectOverlappingAutoUnits = (graphType: OpenSee.graphType) => createSelector( - (state: OpenSee.IRootState) => state.Data.Plots, - (state: OpenSee.IRootState) => state.EventInfo.EventID, - (plots, evtID) => { - let overlappingPlots = plots.filter(p => p.key.DataType === graphType && p.key.EventId !== evtID); - let result = {}; - if (overlappingPlots.length > 0) { - overlappingPlots.forEach(plot => { - let units = {} - Object.keys(plot.yLimits).forEach(key => { - units[key] = plot.yLimits[key].isAuto; - }) - result[plot.key.DataType] = units as OpenSee.IUnitCollection - }) - - return result; - } - - } -); - -export const SelectIsZoomed = (key: OpenSee.IGraphProps,) => createSelector( - (state: OpenSee.IRootState) => state.Data.Plots, - (plots) => { - let plot = plots.find(p => p.key.DataType === key.DataType && p.key.EventId === key.EventId); - return plot?.isZoomed; - } -); - - -// For tooltip -export const SelectHoverPoints = (hover: [number, number]) => createSelector( - (state: OpenSee.IRootState) => state.EventInfo.EventID, - (state: OpenSee.IRootState) => state.Data, - (eventID, state) => { - let result: OpenSee.IPoint[] = []; - - let filteredPlots = state.Plots.filter(plot => plot.key.EventId === eventID) - - filteredPlots.forEach(plot => { - if (plot.data.length === 0) return; - - let dataIndex = getIndex(hover[0], plot.data[0].DataPoints); - if (isNaN(dataIndex)) - return; - - - result = result.concat(...plot.data.filter(d => d.Enabled).map(d => { - dataIndex = getIndex(hover[0], d.DataPoints); - return { - Color: d.Color, - Unit: defaultSettings.Units[d.Unit].options[plot.yLimits[d.Unit].current], - Value: (dataIndex > (d.DataPoints.length - 1) ? NaN : d.DataPoints[dataIndex][1]), - Name: GetDisplayName(d, plot.key.DataType), - BaseValue: d.BaseValue, - Time: 0, - } - })) - }) - return result; - }); - - -export const SelectDeltaHoverPoints = (hover: [number, number]) => createSelector( - (state: OpenSee.IRootState) => state.EventInfo.EventID, - (state: OpenSee.IRootState) => state.Data, - (eventID, state) => { - let result: OpenSee.IPoint[] = []; - - let filteredPlots = state.Plots.filter(plot => plot.key.EventId === eventID) - - filteredPlots.forEach(plot => { - const selectedData = plot.selectedIndixes; - if (plot.data.length === 0) return; - - let dataIndex = getIndex(hover[0], plot.data[0].DataPoints); - if (isNaN(dataIndex)) - return; - - result = result.concat(...plot.data.filter(d => d.Enabled).map(d => { - dataIndex = getIndex(hover[0], d.DataPoints); - return { - Color: d.Color, - Unit: defaultSettings.Units[d.Unit].options[plot.yLimits[d.Unit].current], - Value: (dataIndex > (d.DataPoints.length - 1) ? NaN : d.DataPoints[dataIndex][1]), - Name: GetDisplayName(d, plot.key.DataType), - PrevValue: (selectedData.length > 0 ? ((selectedData[selectedData.length] - 1) > d.DataPoints.length ? NaN : d.DataPoints[selectedData[selectedData.length - 1]][1]) : NaN), - BaseValue: d.BaseValue, - Time: (selectedData.length > 0 ? ((selectedData[selectedData.length] - 1) > d.DataPoints.length ? NaN : d.DataPoints[selectedData[selectedData.length - 1]][0]) : NaN), - } - - })) - }) - return result; - }); - - - -// For vector -export const SelectVPhases = (hover: [number, number]) => createSelector( - (state: OpenSee.IRootState) => state.EventInfo.EventID, - (state: OpenSee.IRootState) => state.Data, - (eventID, state) => { - - let plot = state.Plots.find(plot => plot.key.DataType == 'Voltage' && plot.key.EventId == eventID); - if (!plot || plot.data.length === 0 || !plot.data.some(d => d.LegendHorizontal == 'Ph')) return []; - - const activeUnits = plot.yLimits; - - let asset = _.uniq(plot.data.filter(item => item.Enabled).map(item => item.LegendGroup)); - let phase = _.uniq(plot.data.filter(item => item.Enabled).map(item => item.LegendVertical)); - - let phaseData = plot.data.find(item => item.LegendHorizontal == 'Ph'); - let pointIndex = phaseData ? getIndex(hover[0], phaseData.DataPoints) : -1; - - if (isNaN(pointIndex) || pointIndex < 0) - return []; - - let result: OpenSee.IVector[] = []; - - asset.forEach(a => { - phase.forEach(p => { - let phaseChannel = plot.data.find(item => item.LegendGroup == a && item.LegendVertical == p && item.LegendHorizontal == 'Ph'); - let magnitudeChannel = plot.data.find(item => item.LegendGroup == a && item.LegendVertical == p && item.LegendHorizontal == 'Pk'); - - if (phaseChannel && magnitudeChannel) { - let phaseValue = pointIndex < phaseChannel.DataPoints.length ? phaseChannel.DataPoints[pointIndex][1] : NaN; - let magValue = pointIndex < magnitudeChannel.DataPoints.length ? magnitudeChannel.DataPoints[pointIndex][1] : NaN; - - result.push({ - Color: phaseChannel.Color, - Unit: defaultSettings.Units.Voltage.options[activeUnits["Voltage"].current], - PhaseUnit: defaultSettings.Units.Angle.options[activeUnits["Angle"].current], - Phase: p, - Asset: a, - Magnitude: magValue, - Angle: phaseValue, - BaseValue: magnitudeChannel.BaseValue - }); - } - }); - }); - - return result; - } -); - - -export const SelectIPhases = (hover: [number, number]) => createSelector( - (state: OpenSee.IRootState) => state.EventInfo.EventID, - (state: OpenSee.IRootState) => state.Data, - (eventID, state) => { - let plot = state.Plots.find(p => p.key.DataType == 'Current' && p.key.EventId == eventID); - if (!plot || plot.data.length === 0 || !plot.data.some(d => d.LegendHorizontal == 'Ph')) return []; - - const activeUnits = plot.yLimits; - let asset = _.uniq(plot.data.filter(item => item.Enabled).map(item => item.LegendGroup)); - let phase = _.uniq(plot.data.filter(item => item.Enabled).map(item => item.LegendVertical)); - - - let pointIndex = getIndex(hover[0], plot.data.find(item => item.LegendHorizontal == 'Ph').DataPoints); - if (isNaN(pointIndex)) return []; - - let result: OpenSee.IVector[] = []; - - asset.forEach(a => { - phase.forEach(p => { - let phaseChannel = plot.data.find(item => item.LegendGroup == a && item.LegendVertical == p && item.LegendHorizontal == 'Ph'); - let magnitudeChannel = plot.data.find(item => item.LegendGroup == a && item.LegendVertical == p && item.LegendHorizontal == 'Pk'); - - if (phaseChannel && magnitudeChannel) { - let phaseValue = pointIndex < phaseChannel.DataPoints.length ? phaseChannel.DataPoints[pointIndex][1] : NaN; - let magValue = pointIndex < magnitudeChannel.DataPoints.length ? magnitudeChannel.DataPoints[pointIndex][1] : NaN; - - result.push({ - Color: phaseChannel.Color, - Unit: defaultSettings.Units.Current.options[activeUnits["Current"].current], - PhaseUnit: defaultSettings.Units.Angle.options[activeUnits["Angle"].current], - Phase: p, - Asset: a, - Magnitude: magValue, - Angle: phaseValue, - BaseValue: magnitudeChannel.BaseValue - }); - } - }); - }); - - return result; - } -); - -// For Accumulated Point widget -export const SelectSelectedPoints = createSelector( - (state: OpenSee.IRootState) => state.EventInfo.EventID, - (state: OpenSee.IRootState) => state.Data, - (eventID, state) => { - let result: OpenSee.IPointCollection[] = []; - - state.Plots.forEach(plot => { - if (plot.key.EventId != eventID) return; - if (plot.key.DataType != 'Voltage' && plot.key.DataType != 'Current') return; - if (plot.data.length == 0) return; - - result = result.concat(...plot.data.filter(d => d.Enabled).map(d => { - const unitType = d?.Unit; - const unitOptions = defaultSettings.Units[unitType]?.options ?? {}; - - return { - Group: d.LegendGroup, - Name: (plot.key.DataType == 'Voltage' ? 'V ' : 'I ') + d.LegendVertical + ' ' + d.LegendHorizontal, - Unit: unitOptions[plot.yLimits[unitType].current], - Value: plot.selectedIndixes.map(j => d.DataPoints[j]), - BaseValue: d.BaseValue, - Color: d.Color - } - - })) - - }) - return result; - }) - - -// For FFT Table -export const SelectFFTData = createSelector( - (state: OpenSee.IRootState) => state.Data.Plots.find(plot => plot.key.DataType === "FFT" && plot.key.EventId === state.EventInfo.EventID), - (fftPlot) => { - if (fftPlot?.data == null) return []; - const activeUnits = defaultSettings.Units - let asset = _.uniq(fftPlot.data.map(item => item.LegendGroup)); - let phase = _.uniq(fftPlot.data.map(item => item.LegendVertical)); - - if (fftPlot.data.length == 0) return [] - - let result: OpenSee.IFFTSeries[] = []; - - asset.forEach(a => { - phase.forEach(p => { - if (!fftPlot.data.some((item, i) => (item.LegendGroup == a && item.LegendVertical == p))) - return - - let d = fftPlot.data.filter((item, i) => (item.LegendGroup == a && item.LegendVertical == p)); - let phaseChannel = d.find(item => item.LegendHorizontal == 'Ang'); - let magnitudeChannel = d.find(item => item.LegendHorizontal == 'Mag'); - - if (phaseChannel == undefined || magnitudeChannel == undefined) - return; - - result.push({ - Color: phaseChannel.Color, - Unit: activeUnits[magnitudeChannel.Unit].options[fftPlot.yLimits[magnitudeChannel.Unit].current], - PhaseUnit: activeUnits["Angle"].options[fftPlot.yLimits["Angle"].current], - Phase: p, - Asset: a, - Magnitude: magnitudeChannel.DataPoints.map(item => item[1]), - Angle: phaseChannel.DataPoints.map(item => item[1]), - BaseValue: magnitudeChannel.BaseValue, - Frequency: magnitudeChannel.DataPoints.map(item => item[0] * 60.0), - }); - - }) - }) - - return result; - - }) - - -export function getIndex(t: number, data: Array<[number, number]>): number { - if (data) { - if (data.length < 2) - return NaN; - let dP = data[1][0] - data[0][0]; - - if (t < data[0][0]) - return 0; - - if (t > data[data.length - 1][0]) - return (data.length - 1); - let deltaT = t - data[0][0]; - - return Math.floor(deltaT / dP); - } -} - -function applyLocalSettings(plot: OpenSee.IGraphstate) { - - try { - let settings: OpenSee.ISettingsState = JSON.parse(localStorage.getItem('openSee.Settings')); - const unitSettings = settings.Units - - if (unitSettings && Array.isArray(unitSettings)) { - const matchingPlot = unitSettings.find(setting => setting.DataType === plot.key.DataType) - - Object.keys(matchingPlot.Units).forEach(key => { - plot.yLimits[key].current = matchingPlot.Units[key].current - plot.yLimits[key].isAuto = matchingPlot.Units[key].isAuto - }) - } - else if (!Array.isArray(unitSettings)) { //reset unit localstorage settings for old structure - settings.Units = [] - const serializedState = JSON.stringify(settings); - localStorage.setItem('openSee.Settings', serializedState); - } - } catch { } -} - - -function saveSettings(state: OpenSee.IDataState) { - try { - //lets type currentSettings to prevent errors in future - const settings = JSON.parse(localStorage.getItem("openSee.Settings")) - let unitSettings = settings.Units - if (unitSettings === null || unitSettings === undefined) - unitSettings = [] - - plotTypes.forEach(plotType => { - const matchingPlot = state.Plots.find(plot => plot.key.DataType === plotType); - - if (matchingPlot) { - const relevantUnits = matchingPlot.data.filter(data => data.Enabled) - const enabledUnits = _.uniqBy(relevantUnits, "Unit").map(data => data.Unit) - const plot = unitSettings.find(plot => plot.DataType === matchingPlot.key.DataType) - - if (plot === undefined) - unitSettings.push({ DataType: matchingPlot.key.DataType, Units: null }) - - Object.keys(matchingPlot.yLimits).forEach(key => { - if (enabledUnits.includes(key as OpenSee.Unit)) { - let plot = unitSettings.find(plot => plot.DataType === matchingPlot.key.DataType) - const yLimits = matchingPlot.yLimits[key] - if (plot.Units === undefined || plot.Units === null) - plot.Units = {} - plot.Units[key] = { current: yLimits.current, isAuto: yLimits.isAuto } - } - }) - - } - }); - - let currentSettings = JSON.parse(localStorage.getItem("openSee.Settings")) - if (currentSettings === null || currentSettings === undefined) - currentSettings = {} - currentSettings.Units = unitSettings - const serializedState = JSON.stringify(currentSettings) - localStorage.setItem('openSee.Settings', serializedState); - } catch { - // ignore write errors - } -} - -export function getPrimaryAxis(key: OpenSee.IGraphProps) { - if (key.DataType === "Voltage") - return "Voltage" as OpenSee.Unit - else if (key.DataType === "Current") - return "Current" as OpenSee.Unit - else if (key.DataType === "FirstDerivative") - return "VoltageperSecond" //make sure this is correct - else if (key.DataType === "Unbalance") - return "Unbalance" - else if (key.DataType === "THD") - return "THD" - else if (key.DataType === "RemoveCurrent") - return "Current" - else if (key.DataType === "Power") - return "PowerP" - else if (key.DataType === "Impedance") - return "Impedance" - else if (key.DataType === "Frequency") - return "Freq" - else if (key.DataType === "FaultDistance") - return "Distance" - else if (key.DataType === "I2T") - return "Current" - else - return "Voltage" as OpenSee.Unit - -} - -function updateAutoLimits(plot: OpenSee.IGraphstate, startTime: number, endTime: number) { - //only update limits once there is data loaded - if (plot?.data?.length > 0) { - const RelevantAxis = _.uniq(plot.data.map(s => s.Unit)); - RelevantAxis.forEach(axis => { - const autoLimits = !plot.isZoomed && !plot.yLimits[axis].isManual; - if (!autoLimits) - return; - - let filteredData = plot.data.filter(item => item.Unit === axis && item.Enabled); - const newLimits = recomputeDataLimits(startTime, endTime, filteredData, plot.yLimits[axis].current); - if (newLimits) - plot.yLimits[axis].dataLimits = newLimits; - - }); - } - -} - -// #endregion - -// #region [ Helper Functions ] - - -//This Function Recomputes y Limits based on X limits for all states -function recomputeDataLimits(start: number, end: number, data: OpenSee.iD3DataSeries[], activeUnit: number): [number, number] { - - let limitedData = data.map(item => { - let dataPoints = item.DataPoints; - if (item.SmoothDataPoints.length > 0) - dataPoints = item.SmoothDataPoints; - - let indexStart = getIndex(start, dataPoints); - let indexEnd = getIndex(end, dataPoints); - - let factor = defaultSettings.Units[item.Unit].options[activeUnit].factor; - - if (factor === undefined) { //p.u case - factor = 1.0 / item.BaseValue; - } - - let sliced = dataPoints.slice(indexStart, indexEnd) - let dt = sliced.map(p => p[1]).filter(p => !isNaN(p) && isFinite(p)); - - return [Math.min(...dt) * factor, Math.max(...dt) * factor]; - }); - - let yMin = Math.min(...limitedData.map(item => item[0])); - let yMax = Math.max(...limitedData.map(item => item[1])); - - const pad = (yMax - yMin) / 20; - return [yMin - pad, yMax + pad]; - -} - -function recomputeNonAutoLimits(oldLimits: [number, number], newLimits: [number, number], currentLimits: [number, number]): [number, number] { - // Calculate the old range - const oldRange = oldLimits[1] - oldLimits[0]; - - // Calculate the proportional change - const lowerProportion = (newLimits[0] - oldLimits[0]) / oldRange; - const upperProportion = (newLimits[1] - oldLimits[0]) / oldRange; - - - // Apply the proportional change to the current range - const currentRange = currentLimits[1] - currentLimits[0]; - const updatedLowerLimit = currentLimits[0] + lowerProportion * currentRange; - const updatedUpperLimit = currentLimits[0] + upperProportion * currentRange; - return [updatedLowerLimit, updatedUpperLimit]; -} - -function scaleLimitsByFactor(oldIndex, newIndex, unit: OpenSee.Unit, limits: [number, number]): [number, number] { - const oldFactor = defaultSettings.Units[unit].options[oldIndex].factor - const newFactor = defaultSettings.Units[unit].options[newIndex].factor - const change = newFactor / oldFactor - - //need to handle pu factor somehow.. - - return [limits[0] * change, limits[1] * change] -} - -function scaleLimits(oldDataLimits, newDataLimits, zoomedLimits): [number, number] { - // Calculate the range of old and new data limits - const oldRange = oldDataLimits[1] - oldDataLimits[0]; - const newRange = newDataLimits[1] - newDataLimits[0]; - - // Calculate the proportional change - const scale = newRange / oldRange; - - // Apply the proportional change to zoomed limits - const scaledZoomedLowerLimit = zoomedLimits[0] * scale; - const scaledZoomedUpperLimit = zoomedLimits[1] * scale; - - return [scaledZoomedLowerLimit, scaledZoomedUpperLimit]; -} - -//function that Updates the Current Units if they are on auto -function updateActiveUnits(units: OpenSee.IUnitCollection, unit: OpenSee.Unit, data: OpenSee.iD3DataSeries[], startTime: number, endTime: number, manualLimits: [number, number]) { - if (!units[unit].isAuto) - return; - - let relevantData = data.filter(d => d.Unit == unit).map(d => { - let startIndex = getIndex(startTime, d.DataPoints); - let endIndex = getIndex(endTime, d.DataPoints); - return d.DataPoints.slice(startIndex, endIndex); - }) - - - let min = Math.min(...relevantData.map(d => Math.min(...d.map(p => p[1])))); - let max = Math.max(...relevantData.map(d => Math.max(...d.map(p => p[1])))); - - let autoFactor = 0.000001 - - if (manualLimits) { // for the case of auto unit being selected with manualLimits applied - min = manualLimits[0] - max = manualLimits[1] - } - - if (Math.max(max, min) < 1) - autoFactor = 1000 - else if (Math.max(max, min) < 1000) - autoFactor = 1 - else if (Math.max(max, min) < 1000000) - autoFactor = 0.001 - - - //Logic to move on to next if We can not find that Factor - if (defaultSettings.Units[unit].options.findIndex(item => item.factor == autoFactor) >= 0) - return defaultSettings.Units[unit].options.findIndex(item => item.factor == autoFactor) - else { - //Unable to find Factor try moving one down/up - if (autoFactor < 1) - autoFactor = autoFactor * 1000 - else - autoFactor = 1 - - if (defaultSettings.Units[unit].options.findIndex(item => item.factor == autoFactor) >= 0) - return defaultSettings.Units[unit].options.findIndex(item => item.factor == autoFactor) - else - return defaultSettings.Units[unit].options.findIndex(item => item.factor != 0) - } -} - - -// Function that gets a Tooltip Display Name -function GetDisplayName(d: OpenSee.iD3DataSeries, type: OpenSee.graphType) { - if (type == 'Voltage' || type == 'Current') - return d.LegendGroup + (type == 'Voltage' ? ' V ' : ' I ') + d.LegendVertical + ' ' + d.LegendHorizontal; - if (type == 'FirstDerivative') - return d.LegendGroup + ' ' + d.LegendVGroup + ' derrivative ' + d.LegendHorizontal + ' ' + d.LegendVertical; - if (type == 'ClippedWaveforms') - return d.LegendGroup + ' ' + ' clipped WaveForm ' + d.LegendVertical; - if (type == 'Frequency') - return d.LegendGroup + ' Frequency ' + d.LegendVertical; - if (type == 'HighPassFilter') - return d.LegendGroup + ' ' + d.LegendHorizontal + ' HPF ' + d.LegendVertical; - if (type == 'LowPassFilter') - return d.LegendGroup + ' ' + d.LegendHorizontal + ' LPF ' + d.LegendVertical; - - else - return type; - -} - - -// Function to get Default Enabled Traces -function GetDefaults(type: OpenSee.graphType, defaultTraces: OpenSee.IDefaultTrace, defaultVoltage: "L-L" | "L-N", data: OpenSee.iD3DataSeries[]): boolean[] { - - if (type == 'Voltage') - return data.map(item => item.LegendVGroup == defaultVoltage && - ((item.LegendHorizontal == 'Ph' && defaultTraces.Ph) || - (item.LegendHorizontal == 'RMS' && defaultTraces.RMS) || - (item.LegendHorizontal == 'Pk' && defaultTraces.Pk) || - (item.LegendHorizontal == 'W' && defaultTraces.W) - )) - - if (type == 'Current') - return data.map(item => ((item.LegendHorizontal == 'Ph' && defaultTraces.Ph) || - (item.LegendHorizontal == 'RMS' && defaultTraces.RMS) || - (item.LegendHorizontal == 'Pk' && defaultTraces.Pk) || - (item.LegendHorizontal == 'W' && defaultTraces.W) - )) - - if (type == 'FaultDistance') - return data.map(item => - item.LegendVertical == 'Simple' || - item.LegendVertical == 'Reactance' || - item.LegendVertical == 'Takagi' || - item.LegendVertical == 'ModifiedTakagi' || - item.LegendVertical == 'Novosel') - - if (type == 'FirstDerivative') - return data.map(item => ((item.LegendHorizontal == 'W' && defaultTraces.W) || - (item.LegendHorizontal == 'RMS' && defaultTraces.RMS)) - && item.LegendVertical != 'NG' && item.LegendVertical != 'RES') - - if (type == 'ClippedWaveforms') - return data.map(item => item.LegendVertical == 'AN' || item.LegendVertical == 'BN' || item.LegendVertical == 'CN') - - if (type == 'Frequency') - return data.map(item => item.LegendVertical == 'AN' || item.LegendVertical == 'BN' || item.LegendVertical == 'CN') - - if (type == 'HighPassFilter' || type == 'LowPassFilter') - return data.map(item => item.LegendVertical == 'AN' || item.LegendVertical == 'BN' || item.LegendVertical == 'CN') - - if (type == 'MissingVoltage' || type == 'OverlappingWave') - return data.map(item => item.LegendVertical == 'AN' || item.LegendVertical == 'BN' || item.LegendVertical == 'CN') - - if (type == 'Power') - return data.map(item => (item.LegendVertical == 'AN' || item.LegendVertical == 'BN' || item.LegendVertical == 'CN') && item.LegendHorizontal == 'P') - - if (type == 'Impedance') - return data.map(item => (item.LegendVertical == 'AN' || item.LegendVertical == 'BN' || item.LegendVertical == 'CN') && item.LegendHorizontal == 'R') - - if (type == 'RapidVoltage') - return data.map(item => (item.LegendVertical == 'AN' || item.LegendVertical == 'BN' || item.LegendVertical == 'CN')) - - if (type == 'Rectifier') - return data.map(item => item.LegendHorizontal === 'V') - - if (type == 'SymetricComp') - return data.map(item => (item.LegendVertical == 'Pos')) - - if (type == 'THD') - return data.map(item => (item.LegendVertical == 'AN' || item.LegendVertical == 'BN' || item.LegendVertical == 'CN')) - - if (type == 'Unbalance') - return data.map(item => (item.LegendVertical == 'S2/S1')) - - if (type == 'FFT') - return data.map(item => (item.LegendHorizontal == 'Mag' && item.LegendVGroup == 'Volt.')) - - if (type == 'Harmonic') - return data.map(item => (item.LegendHorizontal == 'Mag')) - - if (type == 'RemoveCurrent') - return data.map(item => (item.LegendHorizontal == 'Pre')) - - if (type == 'I2T') - return data.map(item => item.LegendVertical == 'AN' || item.LegendVertical == 'BN' || item.LegendVertical == 'CN') - - return data.map(item => false); -} - -// #endregion` */ \ No newline at end of file diff --git a/src/OpenSEE/Scripts/TSX/store/eventInfoSlice.tsx b/src/OpenSEE/Scripts/TSX/store/eventInfoSlice.tsx deleted file mode 100644 index 226387ab..00000000 --- a/src/OpenSEE/Scripts/TSX/store/eventInfoSlice.tsx +++ /dev/null @@ -1,98 +0,0 @@ -//****************************************************************************************************** -// settingSlice.tsx - 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 1/23/2024 - Preston Crawford -// Refactored event info slice -// -//****************************************************************************************************** - - -import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit' -import { OpenSee } from '../global'; -declare var homePath: string; - - -export const LoadEventInfo = createAsyncThunk("EventInfo/setEventInfo", async (arg: { breakeroperation: string }, thunkAPI) => { - let state = (thunkAPI.getState() as OpenSee.IRootState); - const eventID = state.EventInfo.EventID; - if (eventID && !isNaN(eventID) && eventID !== 0) { - const data = await $.ajax({ - type: "GET", - url: `${homePath}api/OpenSEE/GetHeaderData?eventId=${eventID}${arg.breakeroperation != undefined ? "&breakeroperation=" + arg.breakeroperation : ""}`, - dataType: 'json', - cache: true, - async: true - }) - return data - } - -}) - -export const LoadLookupInfo = createAsyncThunk("EventInfo/setLookupInfo", async (_, thunkAPI) => { - let state = (thunkAPI.getState() as OpenSee.IRootState); - const eventID = state.EventInfo.EventID; - if (eventID && !isNaN(eventID) && eventID !== 0) { - - const data = await $.ajax({ - type: "GET", - url: `${homePath}api/OpenSEE/GetNavData?eventId=${eventID}`, - dataType: 'json', - cache: true, - async: true - }) - return data - } -}) - -const EventInfoReducer = createSlice({ - name: "EventInfo", - initialState: { - EventInfo: null, - LookupInfo: null, - State: 'Idle', - EventID: 1, - } as OpenSee.IEventStore, - reducers: { - SetEventID: (state, action: PayloadAction) => { - state.EventID = action.payload; - }, - }, - extraReducers: (builder) => { - builder.addCase(LoadEventInfo.pending, (state, action) => { - state.State = 'Loading' - }); - builder.addCase(LoadEventInfo.rejected, (state, action) => { - state.State = 'Error' - }); - builder.addCase(LoadEventInfo.fulfilled, (state, action) => { - state.EventInfo = action.payload - state.State = 'Idle' - }) - builder.addCase(LoadLookupInfo.fulfilled, (state, action) => { - state.LookupInfo = action.payload - }) - } -}) - -export const SelectEventInfo = (state: OpenSee.IRootState) => state.EventInfo.EventInfo -export const SelectLookupInfo = (state: OpenSee.IRootState) => state.EventInfo.LookupInfo -export const SelectEventID = (state: OpenSee.IRootState) => state.EventInfo.EventID - -export const { SetEventID } = EventInfoReducer.actions; - -export default EventInfoReducer.reducer \ No newline at end of file diff --git a/src/OpenSEE/Scripts/TSX/store/overlappingEventsSlice.tsx b/src/OpenSEE/Scripts/TSX/store/overlappingEventsSlice.tsx deleted file mode 100644 index e48e9d44..00000000 --- a/src/OpenSEE/Scripts/TSX/store/overlappingEventsSlice.tsx +++ /dev/null @@ -1,152 +0,0 @@ -//****************************************************************************************************** -// eventSlice.tsx - 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 11/23/2020 - C. Lackner -// Generated original version of source code. -// -//****************************************************************************************************** -import { createSlice, PayloadAction, createAsyncThunk, createSelector } from '@reduxjs/toolkit'; -import { OpenSee } from '../global'; -import * as _ from 'lodash'; -import { AddPlot, RemovePlot } from './dataSlice'; -import { CancelEvent } from './RequestHandler'; - - -export const LoadOverlappingEvents = createAsyncThunk('Event/LoadOverlappingEvents', async (_, thunkAPI) => { - const state = (thunkAPI.getState() as OpenSee.IRootState) - const evtID = state.EventInfo.EventID - - if (evtID && !isNaN(evtID) && evtID !== 0) { - let handle = getOverlappingEvents(evtID, null, null); - return await handle; - } - -}) - - -export const EnableOverlappingEvent = createAsyncThunk('Event/EnableOverlappingEvent', (arg: number, thunkAPI) => { - const state = (thunkAPI.getState() as OpenSee.IRootState); - - const plotIndex = state.OverlappingEvents.EventList.findIndex(event => event.EventID === arg); - - if (plotIndex === -1) - return; - - CancelEvent(arg); - - let plots = _.uniq(state.Data.Plots.map(item => item.key.DataType)); - - if (state.OverlappingEvents.EventList[plotIndex].Selected) - plots.forEach(item => thunkAPI.dispatch(RemovePlot({ DataType: item, EventId: arg }))) - else - plots.forEach(item => thunkAPI.dispatch(AddPlot({ key: {DataType: item, EventId: arg}}))) - - - thunkAPI.dispatch(OverlappingEventReducer.actions.UpdateEnabled(plotIndex)); - - return; -}); - - - -export const OverlappingEventReducer = createSlice({ - name: 'Event', - initialState: { - EventList:[ ], - Loading: false - } as OpenSee.IOverlappingEventsStore, - reducers: { - UpdateEnabled: (state, action: PayloadAction) => { - state.EventList[action.payload].Selected = !state.EventList[action.payload].Selected - }, - SetOverlappingEventList: (state, action: PayloadAction<[number]>) => { - action.payload.forEach(id => { - const evt = state.EventList.find(evt => evt.EventID === id) - if(evt === undefined) - state.EventList.push({ Selected: true, AssetName: "", MeterName: "", EventID: id, StartTime: 0, EventType: "", Inception: 0, DurationEndTime: 0, EndTime: 0 }) - }) - }, - }, - extraReducers: (builder) => { - builder.addCase(LoadOverlappingEvents.pending, (state, action) => { - state.Loading = true; - return state - }); - builder.addCase(LoadOverlappingEvents.fulfilled, (state, action) => { - state.Loading = false; - action.payload.forEach(event => { - let evt = state.EventList.find(evt => evt.EventID === event.EventID) - if (evt === undefined) - state.EventList.push({ Selected: false, AssetName: event.AssetName, MeterName: event.MeterName, EventID: event.EventID, StartTime: new Date(event.StartTime + "Z").getTime(), EndTime: new Date(event.EndTime + "Z").getTime(), EventType: event.EventType, Inception: event.Inception, DurationEndTime: event.DurationEndTime }) - else { - //update eventIDs that were pushed from queryString - evt.AssetName = event.AssetName; - evt.MeterName = event.MeterName; - evt.StartTime = new Date(event.StartTime + "Z").getTime(); - evt.EndTime = new Date(event.EndTime + "Z").getTime(); - evt.EventType = event.EventType; - evt.Inception = event.Inception; - evt.DurationEndTime = event.DurationEndTime; - } - }) - return state - }); - - } -}); - -export const { SetOverlappingEventList } = OverlappingEventReducer.actions; -export default OverlappingEventReducer.reducer; - -export const SelectEventList = (state: OpenSee.IRootState) => state.OverlappingEvents.EventList; -export const SelectEventListLoading = (state: OpenSee.IRootState) => state.OverlappingEvents.Loading; - -export const SelectedOverlappingEventIds = createSelector( - (state: OpenSee.IRootState) => state.OverlappingEvents.EventList, - (eventList) => { - if (eventList.length > 0) { - let evtList = [] - eventList.forEach(evt => { - if (evt.Selected) - evtList.push({ EventID: evt.EventID }) - }) - return evtList - } else - return [] - - } -) - - -function getOverlappingEvents(eventID: number, eventStartTime: string, eventEndTime: string): JQuery.jqXHR { - - let overlappingEventHandle = $.ajax({ - type: "GET", - url: `${homePath}api/OpenSEE/GetOverlappingEvents?eventId=${eventID}` + - `${eventStartTime != undefined ? `&startDate=${eventStartTime}` : ``}` + - `${eventEndTime != undefined ? `&endDate=${eventEndTime}` : ``}`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: true, - async: true - }); - - return overlappingEventHandle; - -} - diff --git a/src/OpenSEE/Scripts/TSX/store/queryThunk.tsx b/src/OpenSEE/Scripts/TSX/store/queryThunk.tsx deleted file mode 100644 index 69dcb2e2..00000000 --- a/src/OpenSEE/Scripts/TSX/store/queryThunk.tsx +++ /dev/null @@ -1,176 +0,0 @@ -//****************************************************************************************************** -// queryThunk.tsx - 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 MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// 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: -// ---------------------------------------------------------------------------------------------------- -// 11/01/2020 - C. Lackner -// Generated original version of source code. -// -// 01/24/2024 - Preston Crawford -// Refactored to incorporate more relevant state into queryParams -// -//****************************************************************************************************** -import { createAsyncThunk } from '@reduxjs/toolkit'; -import { OpenSee } from '../global'; -import * as queryString from "query-string"; -import { AddPlot, SetTimeLimit, SetFFTLimits, SetCycleLimit} from './dataSlice'; -import { SelectEnabledPlots, SetSinglePlot } from './settingSlice'; -import { SetEventID, SelectEventID } from './eventInfoSlice' -import { SelectAnalytics, UpdateAnalytic } from '../store/analyticSlice'; -import { SelectedOverlappingEventIds, SetOverlappingEventList } from '../store/overlappingEventsSlice' - -import * as _ from 'lodash' - - -export const updatedURL = createAsyncThunk('Settings/newURL', (arg: { query: string, initial: boolean }, { getState, dispatch }) => { - let parsedQuery = queryString.parse(arg.query); - - if (parsedQuery) { - if (parsedQuery.plots) { - const plotString = atob(parsedQuery.plots) - parsedQuery.plots = JSON.parse(plotString) - } - if (parsedQuery.overlappingInfo) { - const overlapppingString = atob(parsedQuery.overlappingInfo) - parsedQuery.overlappingInfo = JSON.parse(overlapppingString) - } - } - - - const query: OpenSee.Query = parsedQuery - const oldState = getState() as OpenSee.IRootState; - const enabledPlots = SelectEnabledPlots(oldState); - const oldEventID = SelectEventID(oldState); - const oldAnalytics = SelectAnalytics(oldState); - const oldOverlappingList = SelectedOverlappingEventIds(oldState); - - const analyticQuery = { - Harmonic: ToInt(query.Harmonic), - Trc: ToInt(query.Trc), - LPFOrder: ToInt(query.LPFOrder), - HPFOrder: ToInt(query.HPFOrder), - FFTCycles: ToInt(query.FFTCycles), - FFTStartTime: ToFloat(query.FFTStartTime) - } as OpenSee.IAnalyticStore - - const isAnalyticsEqual = _.isEqual(oldAnalytics, analyticQuery) - const isOverlappingListEqual = _.isEqual(oldOverlappingList, parsedQuery.overlappingInfo) - const isFFTLimitsEqual = _.isEqual([ToInt(query?.FFTLimits?.[0]), ToInt(query?.FFTLimits?.[1])], oldState.Data.fftLimits) - const isCycleLimitsEqual = _.isEqual([ToInt(query?.CycleLimits?.[0]), ToInt(query?.CycleLimits?.[1])], oldState.Data.cycleLimit) - - const noPlots = (query?.plots?.length === 0 && enabledPlots?.length === 0) || query?.plots === undefined - - //Set SinglePlot - if (ToBool(query?.singlePlot) && ToBool(query?.singlePlot) !== undefined) - dispatch(SetSinglePlot(ToBool(query.singlePlot))) - - //Set EventID - if (ToInt(query?.eventID) && !isNaN(query?.eventID) && query?.eventID !== 0 && ToInt(query?.eventID) !== oldEventID) - dispatch(SetEventID(ToInt(query.eventID))) - - //Set TimeLimit - if (ToFloat(query.startTime) != undefined && ToFloat(query.endTime) != undefined && (oldState.Data.startTime != ToFloat(query.startTime) || (oldState.Data.endTime != ToFloat(query.endTime)))) - dispatch(SetTimeLimit({ start: ToFloat(query.startTime), end: ToFloat(query.endTime) })) - - //Set Overlapping EventList - if (!isOverlappingListEqual && query?.overlappingInfo) - dispatch(SetOverlappingEventList(query.overlappingInfo)) - - //Analytic Query - if (!isAnalyticsEqual) - dispatch(UpdateAnalytic({settings: queryStringToNums(analyticQuery)})) - - // On initial load, add default plots (Voltage and Current) if there is none provided via query - if (noPlots && arg.initial) { - dispatch(AddPlot({ key: { EventId: oldState.EventInfo.EventID, DataType: "Voltage" } })); - dispatch(AddPlot({ key: { EventId: oldState.EventInfo.EventID, DataType: "Current" } })); - } - - //TODO: come up with a way to handle traces in queryString CHristoph recommended a grid of some a sort, however this would more than likely require us compressing the queryString / reducing number of plots in queryString - else if (query?.plots?.length > 0) { - query.plots.forEach(plot => { - const plotChange = query.plots.length !== enabledPlots.length - const oldPlot = enabledPlots.find(p => p.key.DataType === plot.key.DataType && p.key.EventId === plot.key.EventId) - const isYLimitsEqual = _.isEqual(plot?.yLimits, oldPlot?.yLimits) - - if (plotChange && ToBool(query?.singlePlot) && ToBool(query?.singlePlot) !== undefined && plot.key.EventId === -1) { - const plots = query.plots.filter(p => p.key.EventId !== -1 && p.key.DataType === plot.key.DataType) - plots.forEach(p => dispatch(AddPlot( - { - key: p.key, - yLimits: !isYLimitsEqual ? plot.yLimits : undefined, - isZoomed: plot.isZoomed, fftLimits: !isFFTLimitsEqual ? [ToInt(query?.FFTLimits?.[0]),ToInt(query?.FFTLimits?.[1])] : undefined, - cycleLimits: !isCycleLimitsEqual ? [ToInt(query?.CycleLimits?.[0]), ToInt(query?.CycleLimits?.[1])] : undefined - }))) - return; - } - - if (plotChange && plot.key.EventId !== -1) - dispatch(AddPlot( - { - key: plot.key, yLimits: !isYLimitsEqual ? plot.yLimits : undefined, - isZoomed: plot.isZoomed, fftLimits: !isFFTLimitsEqual ? [ToInt(query?.FFTLimits?.[0]), ToInt(query?.FFTLimits?.[1])] : undefined, - cycleLimits: !isCycleLimitsEqual ? [ToInt(query?.CycleLimits?.[0]), ToInt(query?.CycleLimits?.[1])] : undefined - })); - }) - } - - -}) - -export function ToInt(arg) { - if (arg == undefined) - return undefined; - let val = parseInt(arg); - if (isNaN(val)) - return undefined; - return val; -} - -export function ToFloat(arg) { - if (arg == undefined) - return undefined; - let val = parseFloat(arg); - if (isNaN(val)) - return undefined; - return val; -} - -function ToBool(arg) { - if (arg == undefined) - return undefined; - if (arg == "True" || arg == "true" || arg == "1") - return true; - if (arg == "False" || arg == "false" || arg == "0") - return false; - return undefined; -} - -function queryStringToNums(arg: OpenSee.IAnalyticStore ) { - if (arg == undefined) - return undefined; - - let query = {}; - Object.keys(arg).forEach(key => { - const num = parseFloat(arg[key]); - if (!isNaN(num)) - query[key] = num; - else - query[key] = arg[key]; - }); - - return query as OpenSee.IAnalyticStore; -} diff --git a/src/OpenSEE/Startup.cs b/src/OpenSEE/Startup.cs index 60e399c3..a9dbe025 100644 --- a/src/OpenSEE/Startup.cs +++ b/src/OpenSEE/Startup.cs @@ -23,95 +23,88 @@ using System; using System.IO; -using System.Reflection; -using System.Web.Http; -using GSF.Diagnostics; -using GSF.IO; -using GSF.Web.Security; -using GSF.Web.Shared; -using Microsoft.Owin; -using Owin; -using static OpenSEE.Common; - -[assembly: OwinStartup(typeof(OpenSEE.Startup))] +using Gemstone.Diagnostics; +using Gemstone.IO; +using Gemstone.Web; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json.Serialization; +using OpenSEE.Security; namespace OpenSEE; public class Startup { - public void Configuration(IAppBuilder app) + public Startup(IConfiguration configuration, IWebHostEnvironment env) { - // Enable GSF role-based security authentication - app.UseAuthentication(s_authenticationOptions); - - OwinLoaded = true; - - // Configure Web API for self-host - HttpConfiguration config = new HttpConfiguration(); - - // Enable GSF session management - config.EnableSessions(s_authenticationOptions); - - // Set configuration to use reflection to setup routes - config.MapHttpAttributeRoutes(); - - app.UseWebApi(config); + SetupTempPath(); + Configuration = configuration; + Env = env; } - private static readonly AuthenticationOptions s_authenticationOptions; + public IWebHostEnvironment Env { get; set; } + public IConfiguration Configuration { get; } - static Startup() + public void ConfigureServices(IServiceCollection services) { - SetupTempPath(); + IMvcBuilder builder = services + .AddControllersWithViews() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; + options.SerializerSettings.ContractResolver = new DefaultContractResolver(); + }); - s_authenticationOptions = new AuthenticationOptions - { - LoginPage = "~/Login", - LogoutPage = "~/Security/logout", - LoginHeader = $"

    {ApplicationName}

    ", - AuthTestPage = "~/AuthTest", - AnonymousResourceExpression = AnonymousResourceExpression, - AuthFailureRedirectResourceExpression = @"^/$|^/.+$" - }; + // Todo: Temp Auth + services.AddAuthentication(TestAuthHandler.AuthenticationScheme) + .AddScheme(TestAuthHandler.AuthenticationScheme, (options) => { }); - AuthenticationOptions = CreateInstance(s_authenticationOptions); - if (!LogEnabled) - return; - - // Retrieve application log path as defined in the config file - string logPath = LogPath; + services.AddMvc(); + } - // Make sure log directory exists - try + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) { - if (!Directory.Exists(logPath)) - Directory.CreateDirectory(logPath); + app.UseDeveloperExceptionPage(); } - catch + else { - logPath = FilePath.GetAbsolutePath(""); + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); } - try - { - Logger.FileWriter.SetPath(logPath); - Logger.FileWriter.SetLoggingFileCount(MaxLogFiles); - } - catch + app.UseForwardedHeaders(new ForwardedHeadersOptions() { - // ignored - } - } + ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost + }); - public static bool OwinLoaded { get; private set; } + app.UseStaticFiles(WebExtensions.StaticFileEmbeddedResources()); + app.UseStaticFiles(); - public static ReadonlyAuthenticationOptions AuthenticationOptions { get; } + app.UseRouting(); - private static T CreateInstance(params object[] args) - { - Type type = typeof(T); - object instance = type.Assembly.CreateInstance(type.FullName!, false, BindingFlags.Instance | BindingFlags.NonPublic, null, args, null, null); - return (T)instance; + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller}/{newaction?}/{id?}", + defaults: new + { + controller = "Home", + action = "Index" + }); + + endpoints.MapControllers(); + }); } private static void SetupTempPath() diff --git a/src/OpenSEE/TempAuth.cs b/src/OpenSEE/TempAuth.cs new file mode 100644 index 00000000..f33c224d --- /dev/null +++ b/src/OpenSEE/TempAuth.cs @@ -0,0 +1,30 @@ +using System; +using System.Security.Principal; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OpenSEE.Security +{ + public class TestAuthHandlerOptions : AuthenticationSchemeOptions { } + + public class TestAuthHandler : AuthenticationHandler + { + public const string AuthenticationScheme = "Test"; + + public TestAuthHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) { } + + protected override Task HandleAuthenticateAsync() + { + var identity = new GenericIdentity(AuthenticationScheme); + var principal = new GenericPrincipal(identity, ["Administrator"]); + var ticket = new AuthenticationTicket(principal, AuthenticationScheme); + + var result = AuthenticateResult.Success(ticket); + + return Task.FromResult(result); + } + } +} diff --git a/src/OpenSEE/Views/Home/Index.cshtml b/src/OpenSEE/Views/Home/Index.cshtml index 588e3649..9dc9b6e7 100644 --- a/src/OpenSEE/Views/Home/Index.cshtml +++ b/src/OpenSEE/Views/Home/Index.cshtml @@ -24,23 +24,17 @@ // Moved OpenSee out of PQ Dashboard // //*****************************************************************************************************@ -@using GSF.IO; -@using GSF.IO.Checksums; +@using Gemstone.IO; +@using Gemstone.IO.Checksums; +@using System.IO +@using Gemstone.Reflection @{ Layout = ""; - Version assemblyVersionInfo = typeof(OpenSEE.MvcApplication).Assembly.GetName().Version; + Version assemblyVersionInfo = AssemblyInfo.EntryAssembly.Version; string applicationVersion = assemblyVersionInfo.Major + "." + assemblyVersionInfo.Minor + "." + assemblyVersionInfo.Build; } - -@helper ReferenceScript(string path) -{ - string fullPath = FilePath.GetAbsolutePath(path.Substring(2)); - byte[] bytes = File.ReadAllBytes(fullPath); - uint checksum = Crc32.Compute(bytes, 0, bytes.Length); - -} @@ -56,9 +50,8 @@ - - - + + - - - -


    -

    - Logging out...   -

    - Click here if page does not redirect -

    - - - - - - - - diff --git a/src/OpenSEE/Views/Login/UserInfo.cshtml b/src/OpenSEE/Views/Login/UserInfo.cshtml deleted file mode 100644 index 812f5b45..00000000 --- a/src/OpenSEE/Views/Login/UserInfo.cshtml +++ /dev/null @@ -1,5 +0,0 @@ -@using GSF.Web -@{ - Layout = null; -} -@Html.RenderResource("GSF.Web.Shared.Views.UserInfo.cshtml") \ No newline at end of file diff --git a/src/OpenSEE/Web.config b/src/OpenSEE/Web.config deleted file mode 100644 index 6e836d97..00000000 --- a/src/OpenSEE/Web.config +++ /dev/null @@ -1,737 +0,0 @@ - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/OpenSEE/WebExtensions.cs b/src/OpenSEE/WebExtensions.cs index ae3d8788..2b450b60 100644 --- a/src/OpenSEE/WebExtensions.cs +++ b/src/OpenSEE/WebExtensions.cs @@ -21,32 +21,27 @@ // //****************************************************************************************************** +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.FileProviders; - -using GSF.Data.Model; -using GSF.Web.Model; -using System.Collections.Generic; - -namespace OpenSEE +namespace Gesmtone.Web { - - [TableName("OpenSEE.Setting")] - [UseEscapedName] - public class OpenSEESetting : openXDA.Model.Setting { }; - public static class WebExtensions { - public static Dictionary LoadDatabaseSettings(this DataContext dataContext, string scope) + /// + /// Used to create for serving Gemstone.Web embedded javascript and css stylesheets. + /// + /// + public static StaticFileOptions StaticFileEmbeddedResources() { - Dictionary settings = new Dictionary(); - - foreach (OpenSEESetting setting in dataContext.Table().QueryRecords("Name")) + ManifestEmbeddedFileProvider embeddedFileProvider = new(Assembly.GetExecutingAssembly(), "Shared"); + return new StaticFileOptions { - if (!string.IsNullOrEmpty(setting.Name)) - settings.Add(setting.Name, setting.Value); - } - - return settings; + FileProvider = embeddedFileProvider, + RequestPath = new PathString("/@Gemstone") + }; } } } \ No newline at end of file diff --git a/src/OpenSEE/package-lock.json b/src/OpenSEE/package-lock.json index 52d7d0ea..da6d3380 100644 --- a/src/OpenSEE/package-lock.json +++ b/src/OpenSEE/package-lock.json @@ -8,105 +8,100 @@ "name": "opensee", "version": "1.0.0", "dependencies": { - "@gpa-gemstone/gpa-symbols": "0.0.43", - "@gpa-gemstone/react-forms": "1.1.75", - "@gpa-gemstone/react-interactive": "1.0.135", + "@gpa-gemstone/application-typings": "0.0.98", + "@gpa-gemstone/common-pages": "0.0.180", + "@gpa-gemstone/gpa-symbols": "0.0.62", + "@gpa-gemstone/helper-functions": "0.0.60", + "@gpa-gemstone/react-forms": "1.1.123", + "@gpa-gemstone/react-graph": "1.0.109", + "@gpa-gemstone/react-interactive": "1.0.189", + "@gpa-gemstone/react-table": "1.2.113", + "@popperjs/core": "^2.11.8", "@reduxjs/toolkit": "1.8.3", - "@types/d3": "7.0.0", - "@types/eonasdan-bootstrap-datetimepicker": "4.17.26", - "@types/flot": "0.0.31", - "@types/jquery": "3.5.6", - "@types/lodash": "4.14.172", - "@types/moment": "2.13.0", - "@types/query-string": "5.1.0", - "@types/react": "^17.0.19", - "@types/react-dom": "16.8.3", - "@types/react-router-dom": "4.3.1", + "bootstrap": "4.6.2", "d3": "7.4.2", "history": "4.7.2", - "jquery": "3.7.1", "lodash": "^4.17.21", "moment": "2.30.1", "node": "^14.0.0", "query-string": "5.1.1", "raf": "3.4.0", - "react": "^18.0.2", + "react": "18.2.0", "react-color": "2.19.3", "react-dom": "18.2.0", "react-redux": "8.0.2", - "react-router-dom": "6.2.1", "reselect": "^4.0.0", - "styled-components": "^5.3.3", "typestyle": "2.0.1" }, "devDependencies": { + "@types/d3": "7.0.0", + "@types/eonasdan-bootstrap-datetimepicker": "4.17.26", + "@types/flot": "0.0.31", + "@types/jquery": "3.5.6", + "@types/lodash": "4.14.172", + "@types/moment": "2.13.0", + "@types/node": "^25.5.2", + "@types/query-string": "5.1.0", + "@types/react": "^17.0.19", + "@types/react-dom": "16.8.3", + "@types/react-router-dom": "4.3.1", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", "css-loader": "6.2.0", "eslint": "^8.43.0", + "eslint-plugin-react-hooks": "^7.0.1", "fork-ts-checker-webpack-plugin": "^9.0.2", - "node-polyfill-webpack-plugin": "1.1.3", "path": "0.12.7", "source-map-loader": "3.0.0", "style-loader": "3.2.1", - "terser-webpack-plugin": "5.1.3", + "terser-webpack-plugin": "5.3.17", "ts-loader": "9.2.5", "typescript": "5.5.3", "url-loader": "4.1.1", - "webpack": "^5.47.0", + "webpack": "5.106.2", "webpack-cli": "^4.7.2" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.8.tgz", - "integrity": "sha512-c4IM7OTg6k1Q+AJ153e2mc2QVTezTwnb4VzquwcyiEzGnW0Kedv4do/TrkU98qPeC5LNiMt/QXwIjzYXLBpyZg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.8.tgz", - "integrity": "sha512-6AWcmZC/MZCO0yKys4uhg5NlxL0ESF3K6IAaoQ+xSXvPyPyxNWRafP+GDbI88Oh68O7QkJgmEtedWPM9U0pZNg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", "peer": true, "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.8", - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-module-transforms": "^7.24.8", - "@babel/helpers": "^7.24.8", - "@babel/parser": "^7.24.8", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.8", - "@babel/types": "^7.24.8", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -122,15 +117,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -138,25 +133,26 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", - "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.24.8", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -165,17 +161,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", + "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.29.0", "semver": "^6.3.1" }, "engines": { @@ -185,41 +181,50 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -229,35 +234,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -266,78 +271,66 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", - "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", - "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "license": "MIT", "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.8" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -398,12 +391,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -437,12 +430,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -554,12 +547,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -569,14 +562,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", - "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-simple-access": "^7.25.9" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -586,16 +578,16 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz", - "integrity": "sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -605,16 +597,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", - "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-typescript": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -624,56 +616,54 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", - "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -689,37 +679,42 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/@emotion/is-prop-valid": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", - "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "license": "MIT", "dependencies": { - "@emotion/memoize": "^0.8.1" + "@emotion/memoize": "0.7.4" } }, "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT" }, "node_modules/@emotion/stylis": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", + "license": "MIT" }, "node_modules/@emotion/unitless": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -736,9 +731,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -776,26 +771,10 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -805,19 +784,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", @@ -829,201 +795,163 @@ } }, "node_modules/@gpa-gemstone/application-typings": { - "version": "0.0.78", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/application-typings/-/application-typings-0.0.78.tgz", - "integrity": "sha512-PROI6JMKB5+a9thXclmrutHdWBu7lK0y0K3DHnioumenCgVnexj19+ksCi5aAZritqzE0NjD9aNY5xKmQnZ0eg==", + "version": "0.0.98", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/application-typings/-/application-typings-0.0.98.tgz", + "integrity": "sha512-hDSWHjwBqmAebZoY5tbsQoFjEjXAoCznRZvgyRs9bF3ELdCtSKWc8diQAkEkbolXui4bnGNp5meNVrHsfnWniw==", + "license": "MIT", + "peerDependencies": { + "react": "18.2.0" + } + }, + "node_modules/@gpa-gemstone/common-pages": { + "version": "0.0.180", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/common-pages/-/common-pages-0.0.180.tgz", + "integrity": "sha512-Im4aK+o6d1+EJQvcCdrLrjaXVmm4n90qGuMWT3Ir4gIZ+mBRPB6/57YhIzwRz+75htxAtcWIt5vwc2nfeNTXHA==", "license": "MIT", "dependencies": { - "@types/react": "^17.0.14", - "@types/webpack": "^5.28.0", - "react": "^18.2.0" + "@gpa-gemstone/application-typings": "0.0.98", + "@gpa-gemstone/gpa-symbols": "0.0.62", + "@gpa-gemstone/helper-functions": "0.0.60", + "@gpa-gemstone/react-forms": "1.1.123", + "@gpa-gemstone/react-interactive": "1.0.189", + "@gpa-gemstone/react-table": "1.2.113", + "@reduxjs/toolkit": "1.8.3", + "crypto-js": "^4.2.0", + "moment": "^2.29.4", + "moment-timezone": "0.5.43", + "react-redux": "8.0.2", + "styled-components": "5.3.3" + }, + "peerDependencies": { + "react": "18.2.0", + "react-dom": "18.2.0" } }, "node_modules/@gpa-gemstone/gpa-symbols": { - "version": "0.0.43", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/gpa-symbols/-/gpa-symbols-0.0.43.tgz", - "integrity": "sha512-wlVBeeR4u+ULpbmEu/tIQbawLmUilFoVRB3cLm7KC90QRPvSJeJoC9nBzUstW+INvY91DIQmka6M8ittdOvvhg==", + "version": "0.0.62", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/gpa-symbols/-/gpa-symbols-0.0.62.tgz", + "integrity": "sha512-mLmHiaas5A3x1OihSJSpzHnHUbPJidG8qyM4vnyeWNfJ1omd3lima5k4PHDQT7udmsT/R9PwG7M6pQ3vQNDa3g==", "license": "MIT", "dependencies": { "@babel/preset-typescript": "^7.14.5", "babel-jest": "^29.0.0", "babel-loader": "^8.2.2", "jest-cli": "^29.0.0", - "react": "18.2.0", "webpack": "^5.47.0", "webpack-cli": "^4.7.2", "yarn": "^1.22.11" - } - }, - "node_modules/@gpa-gemstone/gpa-symbols/node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" }, - "engines": { - "node": ">=0.10.0" + "peerDependencies": { + "react": "18.2.0" } }, "node_modules/@gpa-gemstone/helper-functions": { - "version": "0.0.35", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/helper-functions/-/helper-functions-0.0.35.tgz", - "integrity": "sha512-Yp2JfUC1YCeyQLoxjir6HRNsy2aRQtFtlkR3HFvHiTQnIAH52ZwCFnM3pjsL6aWwWwkVU9sloLY758ATKdrY4A==", + "version": "0.0.60", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/helper-functions/-/helper-functions-0.0.60.tgz", + "integrity": "sha512-LpTMMRuYQGqEXaPy+gLMptWbC0u0jzBnnNTl6GkfImX5VeKJxhKzm4047mVacv2Sm0Pk9U8mExpnJqX49lQRgg==", "license": "MIT", "dependencies": { - "lodash": "^4.17.21", - "react": "^18.2.0" + "@gpa-gemstone/application-typings": "0.0.98", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "18.2.0" } }, "node_modules/@gpa-gemstone/react-forms": { - "version": "1.1.75", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-forms/-/react-forms-1.1.75.tgz", - "integrity": "sha512-wtkVTzfnay0PS6tL/tI+LzNwAdylnzEs38t55m/M8rMIuzaU4st4w/V5Ou25a6cwpQeW0rg38rkzYXEsdpZzAg==", + "version": "1.1.123", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-forms/-/react-forms-1.1.123.tgz", + "integrity": "sha512-P01u5evo/UrbJ9ZDSJVvYCyOBGCfIOsq2a1LPg+VlkPIDehkSlypu3pl9dEEYd3wldfKngRoJx4ZLvdPSFkeAg==", "license": "MIT", "dependencies": { - "@gpa-gemstone/application-typings": "0.0.78", - "@gpa-gemstone/gpa-symbols": "0.0.43", - "@gpa-gemstone/helper-functions": "0.0.35", - "@types/react": "^17.0.14", - "@types/styled-components": "^5.1.11", + "@gpa-gemstone/application-typings": "0.0.98", + "@gpa-gemstone/gpa-symbols": "0.0.62", + "@gpa-gemstone/helper-functions": "0.0.60", "lodash": "^4.17.21", "moment": "2.29.4", - "react": "^18.2.0", "react-color": "^2.19.3", "react-portal": "4.2.2", "styled-components": "5.3.3" + }, + "peerDependencies": { + "react": "18.2.0" } }, - "node_modules/@gpa-gemstone/react-forms/node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/@gpa-gemstone/react-forms/node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" - }, "node_modules/@gpa-gemstone/react-forms/node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "license": "MIT", "engines": { "node": "*" } }, - "node_modules/@gpa-gemstone/react-forms/node_modules/styled-components": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz", - "integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==", + "node_modules/@gpa-gemstone/react-graph": { + "version": "1.0.109", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-graph/-/react-graph-1.0.109.tgz", + "integrity": "sha512-FXgUXEYKV9iOgV36a7R8MqF9icvwivHBFvuiCn/w+KczDRm951Y3AVvfg8ylQK6Z2ArFnHaaaOUtvnjvN5x2Ww==", + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^0.8.8", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" + "@gpa-gemstone/gpa-symbols": "0.0.62", + "@gpa-gemstone/helper-functions": "0.0.60", + "@gpa-gemstone/react-forms": "1.1.123", + "html2canvas": "^1.4.1", + "lodash": "^4.17.21", + "moment": "2.29.4" }, "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" + "react": "18.2.0", + "react-dom": "18.2.0" + } + }, + "node_modules/@gpa-gemstone/react-graph/node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "license": "MIT", + "engines": { + "node": "*" } }, "node_modules/@gpa-gemstone/react-interactive": { - "version": "1.0.135", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-interactive/-/react-interactive-1.0.135.tgz", - "integrity": "sha512-iMswqrhQk/Krfs3KmZruOu5NA1E7r2oNOghDfy7STKJeRWdN2r3mVwWTiHzMjtd+IU4qqt4fVY/VEfR26p54CA==", + "version": "1.0.189", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-interactive/-/react-interactive-1.0.189.tgz", + "integrity": "sha512-6+LRuGDDkn+zvcZxv/CtKSPV0mDn4uQfK6iGRNo+A3yNG7LVWixOiM1l4I56Z5Pzi2VtCqBGtK7Du2i/Pi8V/Q==", "license": "MIT", "dependencies": { - "@gpa-gemstone/application-typings": "0.0.78", - "@gpa-gemstone/gpa-symbols": "0.0.43", - "@gpa-gemstone/helper-functions": "0.0.35", - "@gpa-gemstone/react-forms": "1.1.75", - "@gpa-gemstone/react-table": "1.2.56", + "@gpa-gemstone/application-typings": "0.0.98", + "@gpa-gemstone/gpa-symbols": "0.0.62", + "@gpa-gemstone/helper-functions": "0.0.60", + "@gpa-gemstone/react-forms": "1.1.123", "@reduxjs/toolkit": "1.8.3", "jquery": "^3.6.0", "lodash": "^4.17.21", - "react": "^18.2.0", "react-portal": "4.2.2", "react-redux": "8.0.2", - "react-router-dom": "6.2.1", + "react-router-dom": "6.30.03", "styled-components": "5.3.3" - } - }, - "node_modules/@gpa-gemstone/react-interactive/node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/@gpa-gemstone/react-interactive/node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "license": "MIT" - }, - "node_modules/@gpa-gemstone/react-interactive/node_modules/styled-components": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz", - "integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^0.8.8", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" }, "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" + "leaflet": "1.9.4", + "react": "18.2.0", + "react-dom": "18.2.0" } }, "node_modules/@gpa-gemstone/react-table": { - "version": "1.2.56", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-table/-/react-table-1.2.56.tgz", - "integrity": "sha512-a6Fj54H77AxU3tYHhv/ZfEwntXruywZuJEgRjQ0GKELn7n2v1XqyZdmY9l83JxkMDC3ZVMq1SWSU3kte7ccdcA==", + "version": "1.2.113", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-table/-/react-table-1.2.113.tgz", + "integrity": "sha512-ovloO6/ONPHpsvzSQO73OiPbK1A8G9Rm/4bMq1uDY88M7+GxrY9qhQg6UGyXzfXyCe3Oq5BSX0+oTjX0LKXMXw==", "license": "MIT", "dependencies": { - "@gpa-gemstone/gpa-symbols": "0.0.43", - "@gpa-gemstone/helper-functions": "0.0.35", - "@types/lodash": "^4.14.171", - "@types/react": "^17.0.14", + "@gpa-gemstone/gpa-symbols": "0.0.62", + "@gpa-gemstone/helper-functions": "0.0.60", + "@gpa-gemstone/react-interactive": "1.0.189", "lodash": "^4.17.21", - "react": "^18.2.0" + "react-portal": "4.2.2" + }, + "peerDependencies": { + "react": "18.2.0", + "react-dom": "18.2.0" } }, "node_modules/@humanwhocodes/config-array": { @@ -1068,6 +996,7 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "license": "MIT", "peerDependencies": { "react": "*" } @@ -1089,9 +1018,9 @@ } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "license": "MIT", "engines": { "node": ">=8" @@ -1276,15 +1205,6 @@ } } }, - "node_modules/@jest/reporters/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", @@ -1301,25 +1221,10 @@ "node": ">=10" } }, - "node_modules/@jest/reporters/node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jest/reporters/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1328,21 +1233,6 @@ "node": ">=10" } }, - "node_modules/@jest/reporters/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -1443,52 +1333,55 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1532,10 +1425,21 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@reduxjs/toolkit": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.3.tgz", "integrity": "sha512-lU/LDIfORmjBbyDLaqFN2JB9YmAT1BElET9y0ZszwhSBa5Ef3t6o5CrHupw5J1iOXwd+o92QfQZ8OJpwXvsssg==", + "license": "MIT", "dependencies": { "immer": "^9.0.7", "redux": "^4.1.2", @@ -1555,10 +1459,19 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "license": "MIT" }, "node_modules/@sinonjs/commons": { @@ -1593,9 +1506,9 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -1612,18 +1525,20 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/d3": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.0.0.tgz", "integrity": "sha512-7rMMuS5unvbvFCJXAkQXIxWTo2OUlmVXN5q7sfQFesuVICY55PSP6hhbUhWjTTNpfTTB3iLALsIYDFe7KUNABw==", + "dev": true, + "license": "MIT", "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", @@ -1658,14 +1573,18 @@ } }, "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-axis": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", "dependencies": { "@types/d3-selection": "*" } @@ -1674,6 +1593,8 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", "dependencies": { "@types/d3-selection": "*" } @@ -1681,17 +1602,23 @@ "node_modules/@types/d3-chord": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==" + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-contour": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" @@ -1700,17 +1627,23 @@ "node_modules/@types/d3-delaunay": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-dispatch": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", - "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==" + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-drag": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", "dependencies": { "@types/d3-selection": "*" } @@ -1718,17 +1651,23 @@ "node_modules/@types/d3-dsv": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==" + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-fetch": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", "dependencies": { "@types/d3-dsv": "*" } @@ -1736,17 +1675,23 @@ "node_modules/@types/d3-force": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==" + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-format": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==" + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-geo": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", "dependencies": { "@types/geojson": "*" } @@ -1754,81 +1699,109 @@ "node_modules/@types/d3-hierarchy": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", "dependencies": { "@types/d3-color": "*" } }, "node_modules/@types/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-polygon": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==" + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-quadtree": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==" + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-random": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==" + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-scale": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", - "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", "dependencies": { "@types/d3-time": "*" } }, "node_modules/@types/d3-scale-chromatic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", - "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-selection": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", - "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==" + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-shape": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", - "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dev": true, + "license": "MIT", "dependencies": { "@types/d3-path": "*" } }, "node_modules/@types/d3-time": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", - "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-time-format": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==" + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-transition": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", - "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", "dependencies": { "@types/d3-selection": "*" } @@ -1837,6 +1810,8 @@ "version": "3.0.8", "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" @@ -1846,15 +1821,18 @@ "version": "4.17.26", "resolved": "https://registry.npmjs.org/@types/eonasdan-bootstrap-datetimepicker/-/eonasdan-bootstrap-datetimepicker-4.17.26.tgz", "integrity": "sha512-5kxvfdwAx8esJTC/N6vxESBAjKprGTdLiTBx4776krLwQicANzkTmMeunnBHZqPx6EdUIkWs/+GSJbmsHrEHjg==", + "dev": true, + "license": "MIT", "dependencies": { "@types/jquery": "*", "moment": ">=2.14.0" } }, "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -1864,29 +1842,34 @@ "version": "3.7.7", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/flot": { "version": "0.0.31", "resolved": "https://registry.npmjs.org/@types/flot/-/flot-0.0.31.tgz", "integrity": "sha512-X+RcMQCqPlQo8zPT6cUFTd/PoYBShMQlHUeOXf05jWlfYnvLuRmluB9z+2EsOKFgUzqzZve5brx+gnFxBaHEUw==", + "dev": true, + "license": "MIT", "dependencies": { "@types/jquery": "*" } }, "node_modules/@types/geojson": { - "version": "7946.0.14", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", - "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/graceful-fs": { "version": "4.1.9", @@ -1898,21 +1881,22 @@ } }, "node_modules/@types/history": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/history/-/history-5.0.0.tgz", - "integrity": "sha512-hy8b7Y1J8OGe6LbAjj3xniQrj3v6lsivCcrmf4TzSgPzLkhIeKgc5IZnT7ReIqmEuodjfO8EYAuoFvIrHi/+jQ==", - "deprecated": "This is a stub types definition. history provides its own type definitions, so you do not need this installed.", - "dependencies": { - "history": "*" - } + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", - "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", "dependencies": { - "@types/react": "*", "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" } }, "node_modules/@types/istanbul-lib-coverage": { @@ -1943,6 +1927,8 @@ "version": "3.5.6", "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.6.tgz", "integrity": "sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg==", + "dev": true, + "license": "MIT", "dependencies": { "@types/sizzle": "*" } @@ -1950,54 +1936,67 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" }, "node_modules/@types/lodash": { "version": "4.14.172", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", - "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==" + "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/moment": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/@types/moment/-/moment-2.13.0.tgz", "integrity": "sha512-DyuyYGpV6r+4Z1bUznLi/Y7HpGn4iQ4IVcGn8zrr1P4KotKLdH0sbK1TFR6RGyX6B+G8u83wCzL+bpawKU/hdQ==", "deprecated": "This is a stub types definition for Moment (https://github.com/moment/moment). Moment provides its own type definitions, so you don't need @types/moment installed!", + "dev": true, + "license": "MIT", "dependencies": { "moment": "*" } }, "node_modules/@types/node": { - "version": "20.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", - "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.18.0" } }, "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" }, "node_modules/@types/query-string": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@types/query-string/-/query-string-5.1.0.tgz", - "integrity": "sha512-9/sJK+T04pNq7uwReR0CLxqXj1dhxiTapZ1tIxA0trEsT6FRS0bz09YMcMb7tsVBTm4RJ0NEBYGsAjoEmqoFXg==" + "integrity": "sha512-9/sJK+T04pNq7uwReR0CLxqXj1dhxiTapZ1tIxA0trEsT6FRS0bz09YMcMb7tsVBTm4RJ0NEBYGsAjoEmqoFXg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/react": { - "version": "17.0.80", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.80.tgz", - "integrity": "sha512-LrgHIu2lEtIo8M7d1FcI3BdwXWoRQwMoXOZ7+dPTW0lYREjmlHl3P0U1VD0i/9tppOuv8/sam7sOjx34TxSFbA==", + "version": "17.0.90", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.90.tgz", + "integrity": "sha512-P9beVR/x06U9rCJzSxtENnOr4BrbJ6VrsrDTc+73TtHv9XHhryXKbjGRB+6oooB2r0G/pQkD/S4dHo/7jUfwFw==", + "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { "version": "16.8.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.8.3.tgz", "integrity": "sha512-HF5hD5YR3z9Mn6kXcW1VKe4AQ04ZlZj1EdLBae61hzQ3eEWWxMgNLUbIxeZp40BnSxqY1eAYLsH9QopQcxzScA==", + "devOptional": true, + "license": "MIT", "peer": true, "dependencies": { "@types/react": "*" @@ -2007,6 +2006,8 @@ "version": "5.1.20", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "license": "MIT", "dependencies": { "@types/history": "^4.7.11", "@types/react": "*" @@ -2016,33 +2017,33 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-4.3.1.tgz", "integrity": "sha512-GbztJAScOmQ/7RsQfO4cd55RuH1W4g6V1gDW3j4riLlt+8yxYLqqsiMzmyuXBLzdFmDtX/uU2Bpcm0cmudv44A==", + "dev": true, + "license": "MIT", "dependencies": { "@types/history": "*", "@types/react": "*", "@types/react-router": "*" } }, - "node_modules/@types/react-router/node_modules/@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" - }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "license": "MIT" }, "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", "dev": true, "license": "MIT" }, "node_modules/@types/sizzle": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", - "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==" + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", + "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", + "dev": true, + "license": "MIT" }, "node_modules/@types/stack-utils": { "version": "2.0.3", @@ -2050,36 +2051,16 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, - "node_modules/@types/styled-components": { - "version": "5.1.34", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz", - "integrity": "sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==", - "dependencies": { - "@types/hoist-non-react-statics": "*", - "@types/react": "*", - "csstype": "^3.0.2" - } - }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" - }, - "node_modules/@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "license": "MIT" }, "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "license": "MIT", "dependencies": { "@types/yargs-parser": "*" @@ -2127,9 +2108,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -2257,9 +2238,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -2297,9 +2278,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -2328,9 +2309,9 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", - "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, "license": "ISC" }, @@ -2484,6 +2465,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "license": "MIT", "peerDependencies": { "webpack": "4.x.x || 5.x.x", "webpack-cli": "4.x.x" @@ -2493,6 +2475,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "license": "MIT", "dependencies": { "envinfo": "^7.7.3" }, @@ -2504,6 +2487,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "license": "MIT", "peerDependencies": { "webpack-cli": "4.x.x" }, @@ -2530,12 +2514,13 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "peer": true, "bin": { @@ -2545,6 +2530,18 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -2556,9 +2553,10 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", @@ -2571,10 +2569,50 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } @@ -2594,6 +2632,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2622,6 +2672,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -2649,51 +2700,6 @@ "node": ">=8" } }, - "node_modules/asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2765,25 +2771,10 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/babel-plugin-styled-components": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", - "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "lodash": "^4.17.21", - "picomatch": "^2.3.1" - }, - "peerDependencies": { - "styled-components": ">= 2" - } - }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", @@ -2803,7 +2794,7 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { @@ -2825,58 +2816,62 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", "engines": { "node": "*" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node_modules/bootstrap": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", + "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", + "deprecated": "This version of Bootstrap is no longer supported. Please upgrade to the latest version.", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "jquery": "1.9.1 - 3", + "popper.js": "^1.16.1" } }, - "node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2886,6 +2881,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -2893,129 +2889,10 @@ "node": ">=8" } }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true - }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", - "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", - "dev": true, - "dependencies": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.5", - "hash-base": "~3.0", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.7", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/browserify-sign/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "dependencies": { - "pako": "~1.0.5" - } - }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -3033,10 +2910,11 @@ "license": "MIT", "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -3054,70 +2932,17 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true - }, - "node_modules/builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } @@ -3135,14 +2960,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001683", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001683.tgz", - "integrity": "sha512-iqmNnThZ0n70mNwvxpEC2nBJ037ZHZUoBI5Gorh1Mw6IlEAZujEoU1tXA628iZfzm7R9FvFzxbfdgml82a3k8Q==", + "version": "1.0.30001767", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", + "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", "funding": [ { "type": "opencollective", @@ -3175,27 +3001,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -3206,33 +3011,26 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", "engines": { "node": ">=6.0" } @@ -3252,20 +3050,10 @@ "node": ">=8" } }, - "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/cjs-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", - "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "license": "MIT" }, "node_modules/cliui": { @@ -3286,6 +3074,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", @@ -3306,9 +3095,9 @@ } }, "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", "license": "MIT" }, "node_modules/color-convert": { @@ -3330,14 +3119,16 @@ "license": "MIT" }, "node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", "engines": { "node": ">= 10" } @@ -3351,36 +3142,21 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "node_modules/constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, + "license": "MIT", "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", @@ -3406,13 +3182,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3420,49 +3198,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -3498,41 +3233,36 @@ "node": ">= 8" } }, - "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - }, - "engines": { - "node": "*" - } + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", "engines": { "node": ">=4" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.2.0.tgz", "integrity": "sha512-/rvHfYRjIpymZblf49w8jYcRo2y9gj6rV8UroHGmBxKrIyGLokpycyKzp9OkitvqT29ZSpzJ0Ic7SpnJX3sC8g==", "dev": true, + "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.2.15", @@ -3555,10 +3285,11 @@ } }, "node_modules/css-loader/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3570,6 +3301,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", @@ -3581,6 +3313,7 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -3589,14 +3322,16 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" }, "node_modules/d3": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/d3/-/d3-7.4.2.tgz", "integrity": "sha512-7VK+QBAWtNDbP2EU/ThkXgjd0u1MsXYYgCK2ElQ4BBWh0usE75tHVVeYx47m2pqQEy4isYKAA0tAFSln0l+9EQ==", + "license": "ISC", "dependencies": { "d3-array": "3", "d3-axis": "3", @@ -3637,6 +3372,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", "dependencies": { "internmap": "1 - 2" }, @@ -3648,6 +3384,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3656,6 +3393,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", @@ -3671,6 +3409,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", "dependencies": { "d3-path": "1 - 3" }, @@ -3682,6 +3421,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3690,6 +3430,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-3.1.0.tgz", "integrity": "sha512-vV3xtwrYK5p1J4vyukr70m57mtFTEQYqoaDC1ylBfht/hkdUF0nfWZ1b3V2EPBUVkUkoqq5/fbRoBImBWJgOsg==", + "license": "ISC", "dependencies": { "d3-array": "2 - 3" }, @@ -3701,6 +3442,7 @@ "version": "6.0.4", "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", "dependencies": { "delaunator": "5" }, @@ -3712,6 +3454,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3720,6 +3463,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" @@ -3732,6 +3476,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", "dependencies": { "commander": "7", "iconv-lite": "0.6", @@ -3756,6 +3501,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", "engines": { "node": ">=12" } @@ -3764,6 +3510,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", "dependencies": { "d3-dsv": "1 - 3" }, @@ -3775,6 +3522,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", @@ -3785,9 +3533,10 @@ } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3796,6 +3545,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", "dependencies": { "d3-array": "2.5.0 - 3" }, @@ -3807,6 +3557,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3815,6 +3566,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", "dependencies": { "d3-color": "1 - 3" }, @@ -3826,6 +3578,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3834,6 +3587,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3842,6 +3596,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3850,6 +3605,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3858,6 +3614,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", @@ -3873,6 +3630,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" @@ -3885,6 +3643,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", "peer": true, "engines": { "node": ">=12" @@ -3894,6 +3653,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { "d3-path": "^3.1.0" }, @@ -3905,6 +3665,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", "dependencies": { "d3-array": "2 - 3" }, @@ -3916,6 +3677,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { "d3-time": "1 - 3" }, @@ -3927,6 +3689,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3935,6 +3698,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", @@ -3953,6 +3717,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", @@ -3965,11 +3730,12 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3984,14 +3750,15 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", "engines": { "node": ">=0.10" } }, "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -4013,62 +3780,20 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" } }, - "node_modules/des.js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", - "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -4087,23 +3812,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4130,46 +3838,12 @@ "node": ">=6.0.0" } }, - "node_modules/domain-browser": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.23.0.tgz", - "integrity": "sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, "node_modules/electron-to-chromium": { - "version": "1.5.64", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.64.tgz", - "integrity": "sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "license": "ISC" }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -4192,27 +3866,29 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.21.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz", + "integrity": "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" } }, "node_modules/envinfo": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", - "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "license": "MIT", "bin": { "envinfo": "dist/cli.js" }, @@ -4221,38 +3897,18 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "license": "MIT" }, "node_modules/escalade": { @@ -4265,12 +3921,16 @@ } }, "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { @@ -4331,10 +3991,31 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -4343,14 +4024,6 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-scope/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -4371,19 +4044,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -4401,6 +4061,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/eslint/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4418,39 +4088,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -4476,22 +4117,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -4508,19 +4133,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -4553,9 +4165,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4565,10 +4177,21 @@ "node": ">=0.10" } }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -4576,10 +4199,20 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { + "node_modules/esrecurse/node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -4598,24 +4231,16 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", "engines": { "node": ">=0.8.x" } }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -4661,12 +4286,13 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -4674,16 +4300,30 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -4692,18 +4332,35 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", "engines": { "node": ">= 4.9.1" } }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -4736,6 +4393,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4743,15 +4401,6 @@ "node": ">=8" } }, - "node_modules/filter-obj": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-2.0.2.tgz", - "integrity": "sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -4773,6 +4422,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -4785,6 +4435,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", "bin": { "flat": "cli.js" } @@ -4805,30 +4456,22 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, "node_modules/fork-ts-checker-webpack-plugin": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", - "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.16.7", "chalk": "^4.1.2", - "chokidar": "^3.5.3", + "chokidar": "^4.0.1", "cosmiconfig": "^8.2.0", "deepmerge": "^4.2.2", "fs-extra": "^10.0.0", @@ -4840,8 +4483,7 @@ "tapable": "^2.2.1" }, "engines": { - "node": ">=12.13.0", - "yarn": ">=1.0.0" + "node": ">=14.21.3" }, "peerDependencies": { "typescript": ">3.6.0", @@ -4853,6 +4495,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -4867,10 +4510,11 @@ } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4881,13 +4525,15 @@ "node_modules/free-style": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/free-style/-/free-style-2.5.1.tgz", - "integrity": "sha512-X7dtUSTrlS1KRQBtiQ618NWIRDdRgD91IeajKCSh0fgTqArSixv+n3ea6F/OSvrvg14tPLR+yCq2s+O602+pRw==" + "integrity": "sha512-X7dtUSTrlS1KRQBtiQ618NWIRDdRgD91IeajKCSh0fgTqArSixv+n3ea6F/OSvrvg14tPLR+yCq2s+O602+pRw==", + "license": "MIT" }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -4898,10 +4544,11 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", - "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" }, "node_modules/fs.realpath": { "version": "1.0.0", @@ -4914,6 +4561,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -4926,6 +4574,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4934,6 +4583,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -4947,25 +4597,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -4979,6 +4610,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -4990,7 +4622,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -5008,28 +4640,38 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globby": { @@ -5053,22 +4695,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", @@ -5078,102 +4709,48 @@ "license": "MIT" }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==", + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": ">=4" - } + "license": "MIT" }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, + "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" + "hermes-estree": "0.25.1" } }, "node_modules/history": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", + "license": "MIT", "dependencies": { "invariant": "^2.2.1", "loose-envify": "^1.2.0", @@ -5182,21 +4759,11 @@ "warning": "^3.0.0" } }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } @@ -5204,7 +4771,8 @@ "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/html-escaper": { "version": "2.0.2", @@ -5212,16 +4780,24 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "license": "MIT" }, - "node_modules/https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", - "dev": true + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } @@ -5230,6 +4806,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -5242,6 +4819,7 @@ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true, + "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -5249,26 +4827,6 @@ "postcss": "^8.1.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5283,16 +4841,18 @@ "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -5309,14 +4869,16 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -5354,12 +4916,14 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", "engines": { "node": ">=12" } @@ -5368,6 +4932,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -5376,59 +4941,22 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-core-module": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", - "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -5444,6 +4972,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5466,26 +4995,12 @@ "node": ">=6" } }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -5493,26 +5008,11 @@ "node": ">=0.10.0" } }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -5531,6 +5031,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -5542,6 +5043,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -5549,36 +5051,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5622,15 +5105,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-report/node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -5647,9 +5121,9 @@ } }, "node_modules/istanbul-lib-report/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5658,18 +5132,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", @@ -5685,9 +5147,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -5711,21 +5173,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-changed-files/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-circus": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", @@ -5757,21 +5204,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-cli": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", @@ -5944,45 +5376,6 @@ "fsevents": "^2.3.2" } }, - "node_modules/jest-haste-map/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-haste-map/node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jest-leak-detector": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", @@ -6136,70 +5529,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runner/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner/node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-runner/node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/jest-runner/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jest-runtime": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", @@ -6265,9 +5594,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6342,30 +5671,25 @@ } }, "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", "dependencies": { "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -6380,17 +5704,19 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -6401,9 +5727,9 @@ } }, "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -6422,12 +5748,14 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -6440,6 +5768,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -6448,10 +5777,11 @@ } }, "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -6473,6 +5803,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6486,6 +5817,13 @@ "node": ">=6" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause", + "peer": true + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -6512,20 +5850,27 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -6539,6 +5884,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -6547,14 +5893,16 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -6567,6 +5915,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -6578,6 +5927,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } @@ -6609,24 +5959,15 @@ "node_modules/material-colors": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", - "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", + "license": "ISC" }, "node_modules/memfs": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", "dev": true, + "license": "Unlicense", "dependencies": { "fs-monkey": "^1.0.4" }, @@ -6637,7 +5978,8 @@ "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", @@ -6662,29 +6004,12 @@ "node": ">=8.6" } }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6693,6 +6018,8 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -6704,26 +6031,16 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true - }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6740,15 +6057,28 @@ "node": "*" } }, + "node_modules/moment-timezone": { + "version": "0.5.43", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz", + "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -6756,6 +6086,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -6779,13 +6110,15 @@ "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" }, "node_modules/node": { "version": "14.21.3", "resolved": "https://registry.npmjs.org/node/-/node-14.21.3.tgz", "integrity": "sha512-58OWd3tZhyABy+OwPGawVKmK5tvlM5Z0TATCSLsiVcIp7NFvMahhzABSkBlnvyiISGcxmainzEAv7L9YJI5EMw==", "hasInstallScript": true, + "license": "ISC", "dependencies": { "node-bin-setup": "^1.0.0" }, @@ -6800,12 +6133,14 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-bin-setup": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/node-bin-setup/-/node-bin-setup-1.1.3.tgz", - "integrity": "sha512-opgw9iSCAzT2+6wJOETCpeRYAQxSopqQ2z+N6BXwIMsQQ7Zj5M8MaafQY8JMlolRR6R1UXg2WmhKp0p9lSOivg==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/node-bin-setup/-/node-bin-setup-1.1.4.tgz", + "integrity": "sha512-vWNHOne0ZUavArqPP5LJta50+S8R261Fr5SvGul37HbEDcowvLjwdvd0ZeSr0r2lTSrPxl6okq9QUw8BFGiAxA==", + "license": "ISC" }, "node_modules/node-int64": { "version": "0.4.0", @@ -6813,54 +6148,17 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "license": "MIT" }, - "node_modules/node-polyfill-webpack-plugin": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-1.1.3.tgz", - "integrity": "sha512-yMMGIcVDHN/KT4A+jyGBGojdD1Ht5oIho0CFByEcnsuS0u7euL3Sw9a5s+LLJujP4AP8xSjj/wxBnCj+bluI1A==", - "dev": true, - "dependencies": { - "assert": "^2.0.0", - "browserify-zlib": "^0.2.0", - "buffer": "^6.0.3", - "console-browserify": "^1.2.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.12.0", - "domain-browser": "^4.19.0", - "events": "^3.3.0", - "filter-obj": "^2.0.2", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "^1.0.1", - "process": "^0.11.10", - "punycode": "^2.1.1", - "querystring-es3": "^0.2.1", - "readable-stream": "^3.6.0", - "stream-browserify": "^3.0.0", - "stream-http": "^3.2.0", - "string_decoder": "^1.3.0", - "timers-browserify": "^2.0.12", - "tty-browserify": "^0.0.1", - "url": "^0.11.0", - "util": "^0.12.4", - "vm-browserify": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "webpack": ">=5" - } - }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6869,6 +6167,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -6880,65 +6179,11 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6952,6 +6197,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -6980,21 +6226,16 @@ "node": ">= 0.8.0" } }, - "node_modules/os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true - }, "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7004,6 +6245,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -7011,25 +6253,36 @@ "node": ">=8" } }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -7037,27 +6290,11 @@ "node": ">=6" } }, - "node_modules/parse-asn1": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", - "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", - "dev": true, - "dependencies": { - "asn1.js": "^4.10.1", - "browserify-aes": "^1.2.0", - "evp_bytestokey": "^1.0.3", - "hash-base": "~3.0", - "pbkdf2": "^3.1.2", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -7076,21 +6313,17 @@ "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", "dev": true, + "license": "MIT", "dependencies": { "process": "^0.11.1", "util": "^0.10.3" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", "engines": { "node": ">=8" } @@ -7108,6 +6341,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", "engines": { "node": ">=8" } @@ -7115,13 +6349,15 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7130,37 +6366,24 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/path/node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "2.0.3" } }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", @@ -7169,9 +6392,10 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -7180,9 +6404,9 @@ } }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "license": "MIT", "engines": { "node": ">= 6" @@ -7192,6 +6416,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -7199,19 +6424,22 @@ "node": ">=8" } }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, - "engines": { - "node": ">= 0.4" + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -7227,11 +6455,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -7242,6 +6471,7 @@ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, + "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -7250,13 +6480,14 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", "dev": true, + "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -7267,12 +6498,13 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dev": true, + "license": "ISC", "dependencies": { - "postcss-selector-parser": "^6.0.4" + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" @@ -7286,6 +6518,7 @@ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, + "license": "ISC", "dependencies": { "icss-utils": "^5.0.0" }, @@ -7297,10 +6530,11 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", - "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -7312,7 +6546,8 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -7350,21 +6585,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -7382,6 +6618,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -7391,32 +6628,14 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } @@ -7437,25 +6656,11 @@ ], "license": "MIT" }, - "node_modules/qs": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.3.tgz", - "integrity": "sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/query-string": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "license": "MIT", "dependencies": { "decode-uri-component": "^0.2.0", "object-assign": "^4.1.0", @@ -7465,15 +6670,6 @@ "node": ">=0.10.0" } }, - "node_modules/querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7499,32 +6695,16 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", + "license": "MIT", "dependencies": { "performance-now": "^2.1.0" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "license": "MIT", "peer": true, "dependencies": { "loose-envify": "^1.1.0" @@ -7537,6 +6717,7 @@ "version": "2.19.3", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", + "license": "MIT", "dependencies": { "@icons/material": "^0.2.4", "lodash": "^4.17.15", @@ -7554,6 +6735,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "license": "MIT", "peer": true, "dependencies": { "loose-envify": "^1.1.0", @@ -7564,15 +6746,16 @@ } }, "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "peer": true + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "license": "MIT" }, "node_modules/react-portal": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/react-portal/-/react-portal-4.2.2.tgz", "integrity": "sha512-vS18idTmevQxyQpnde0Td6ZcUlv+pD8GTyR42n3CHUQq9OHi1C4jDE4ZWEbEsrbrLRhSECYiao58cvocwMtP7Q==", + "license": "MIT", "dependencies": { "prop-types": "^15.5.8" }, @@ -7585,6 +6768,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.2.tgz", "integrity": "sha512-nBwiscMw3NoP59NFCXFf02f8xdo+vSHT/uZ1ldDwF7XaTpzm+Phk97VT4urYBl5TYAPNVaFm12UHAEyzkpNzRA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -7619,84 +6803,72 @@ } } }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/react-router": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz", - "integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", "dependencies": { - "history": "^5.2.0" + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz", - "integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", "dependencies": { - "history": "^5.2.0", - "react-router": "6.2.1" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, - "node_modules/react-router-dom/node_modules/history": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", - "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", - "dependencies": { - "@babel/runtime": "^7.7.6" - } - }, - "node_modules/react-router/node_modules/history": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", - "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", - "dependencies": { - "@babel/runtime": "^7.7.6" - } - }, "node_modules/reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "license": "MIT", "dependencies": { "lodash": "^4.0.1" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, + "license": "MIT", "engines": { - "node": ">=8.10.0" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/rechoir": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "license": "MIT", "dependencies": { "resolve": "^1.9.0" }, @@ -7708,6 +6880,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" @@ -7717,15 +6890,11 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "license": "MIT", "peerDependencies": { "redux": "^4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7735,23 +6904,37 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reselect": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7760,6 +6943,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, @@ -7771,6 +6955,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", "engines": { "node": ">=8" } @@ -7778,21 +6963,22 @@ "node_modules/resolve-pathname": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", - "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" + "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==", + "license": "MIT" }, "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -7817,20 +7003,11 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, "node_modules/robust-predicates": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" }, "node_modules/run-parallel": { "version": "1.2.0", @@ -7859,36 +7036,20 @@ "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } @@ -7915,59 +7076,16 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/serialize-javascript": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", - "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", "dependencies": { "kind-of": "^6.0.2" }, @@ -7978,12 +7096,14 @@ "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -7995,32 +7115,16 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" }, "node_modules/sisteransi": { "version": "1.0.5", @@ -8041,15 +7145,17 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -8059,6 +7165,7 @@ "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.0.tgz", "integrity": "sha512-GKGWqWvYr04M7tn8dryIWvb0s8YM41z82iQv01yBtIylgxax0CwvSy6gc2Y02iuXwEfGWRlMicH0nvms9UZphw==", "dev": true, + "license": "MIT", "dependencies": { "abab": "^2.0.5", "iconv-lite": "^0.6.2", @@ -8080,14 +7187,16 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -8111,45 +7220,24 @@ "node": ">=10" } }, - "node_modules/stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dev": true, - "dependencies": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - } - }, - "node_modules/stream-http": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", - "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", - "dev": true, - "dependencies": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "xtend": "^4.0.2" + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -8202,6 +7290,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -8223,6 +7312,7 @@ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.2.1.tgz", "integrity": "sha512-1k9ZosJCRFaRbY6hH49JFlRB0fVSbmnyq1iTPjNxUmGVjBNEmwrrHPenhlp+Lgo51BojHSf6pl2FcqYaN3PfVg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12.13.0" }, @@ -8235,14 +7325,15 @@ } }, "node_modules/styled-components": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", - "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz", + "integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==", + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^1.1.0", + "@emotion/is-prop-valid": "^0.8.8", "@emotion/stylis": "^0.8.4", "@emotion/unitless": "^0.7.4", "babel-plugin-styled-components": ">= 1.12.0", @@ -8264,10 +7355,36 @@ "react-is": ">= 16.8.0" } }, - "node_modules/supports-color": { + "node_modules/styled-components/node_modules/babel-plugin-styled-components": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", + "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "lodash": "^4.17.21", + "picomatch": "^2.3.1" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "node_modules/styled-components/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/styled-components/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -8275,10 +7392,23 @@ "node": ">=4" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8287,91 +7417,174 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/terser": { - "version": "5.31.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.2.tgz", - "integrity": "sha512-LGyRZVFm/QElZHy/CPr/O4eNZOZIzsrQ92y4v9UJe/pFJjypje2yI3C2FmPtvUEnhadlSbmG2nXtdcjHOjCfxw==", + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" + "fast-deep-equal": "^3.1.3" }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" + "peerDependencies": { + "ajv": "^8.8.2" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.3.tgz", - "integrity": "sha512-cxGbMqr6+A2hrIB5ehFIF+F/iST5ZOxvOmy9zih9ySbP1C2oEWQSOUS+2SNBTjzx5xLKO4xnod9eywdfq1Nb9A==", + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, + "license": "MIT", "dependencies": { - "jest-worker": "^27.0.2", - "p-limit": "^3.1.0", - "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1", - "source-map": "^0.6.1", - "terser": "^5.7.0" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "engines": { "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" } }, - "node_modules/terser-webpack-plugin/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, + "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">=10" + "node": ">= 10.13.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } }, "node_modules/test-exclude": { "version": "6.0.0", @@ -8387,6 +7600,15 @@ "node": ">=8" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -8394,22 +7616,11 @@ "dev": true, "license": "MIT" }, - "node_modules/timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dev": true, - "dependencies": { - "setimmediate": "^1.0.4" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" }, "node_modules/tmpl": { "version": "1.0.5", @@ -8421,6 +7632,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -8433,6 +7645,7 @@ "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.5.tgz", "integrity": "sha512-al/ATFEffybdRMUIr5zMEWQdVnCGMUA9d3fXJ8dBVvBlzytPvIszoG9kZoR+94k6/i293RnVOXwMaWbXhNy9pQ==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", @@ -8448,10 +7661,11 @@ } }, "node_modules/ts-loader/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -8482,12 +7696,6 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "node_modules/tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", - "dev": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8511,9 +7719,10 @@ } }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -8527,6 +7736,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, + "license": "Apache-2.0", "peer": true, "bin": { "tsc": "bin/tsc", @@ -8540,6 +7750,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/typestyle/-/typestyle-2.0.1.tgz", "integrity": "sha512-3Mv5ZZbYJ3y3G6rX3iRLrgYibAlafK2nsc9VlTsYcEaK8w+9vtNDx0T2TJsznI5FIh+WoBnjJ5F0/26WaGRxXQ==", + "license": "MIT", "dependencies": { "csstype": "^2.4.0", "free-style": "2.5.1" @@ -8548,26 +7759,29 @@ "node_modules/typestyle/node_modules/csstype": { "version": "2.6.21", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "license": "MIT" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -8585,7 +7799,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -8598,25 +7812,17 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/url": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", - "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", - "dev": true, - "dependencies": { - "punycode": "^1.4.1", - "qs": "^6.11.2" - } - }, "node_modules/url-loader": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", "dev": true, + "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "mime-types": "^2.1.27", @@ -8644,6 +7850,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -8657,43 +7864,30 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/url/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true - }, "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/v8-compile-cache": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", - "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==" + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } }, "node_modules/v8-to-istanbul": { "version": "9.3.0", @@ -8712,13 +7906,8 @@ "node_modules/value-equal": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", - "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" - }, - "node_modules/vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true + "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==", + "license": "MIT" }, "node_modules/walker": { "version": "1.0.8", @@ -8733,14 +7922,16 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", "integrity": "sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==", + "license": "BSD-3-Clause", "dependencies": { "loose-envify": "^1.0.0" } }, "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -8750,35 +7941,36 @@ } }, "node_modules/webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", "license": "MIT", "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" @@ -8797,23 +7989,23 @@ } }, "node_modules/webpack-cli": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.7.2.tgz", - "integrity": "sha512-mEoLmnmOIZQNiRl0ebnjzQ74Hk0iKS5SiEEnpq3dRezoyR3yPaeQZCMCe+db4524pj1Pd5ghZXjT41KLzIhSLw==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", + "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", + "license": "MIT", "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.0.4", - "@webpack-cli/info": "^1.3.0", - "@webpack-cli/serve": "^1.5.1", - "colorette": "^1.2.1", + "@webpack-cli/configtest": "^1.2.0", + "@webpack-cli/info": "^1.5.0", + "@webpack-cli/serve": "^1.7.0", + "colorette": "^2.0.14", "commander": "^7.0.0", - "execa": "^5.0.0", + "cross-spawn": "^7.0.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^2.2.0", "rechoir": "^0.7.0", - "v8-compile-cache": "^2.2.0", "webpack-merge": "^5.7.3" }, "bin": { @@ -8822,6 +8014,10 @@ "engines": { "node": ">=10.13.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, "peerDependencies": { "webpack": "4.x.x || 5.x.x" }, @@ -8844,6 +8040,7 @@ "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", @@ -8854,21 +8051,82 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz", + "integrity": "sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==", + "license": "MIT", "engines": { "node": ">=10.13.0" } }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 10.13.0" @@ -8878,26 +8136,31 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/webpack/node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "license": "BSD-3-Clause", + "node_modules/webpack/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", "dependencies": { - "randombytes": "^2.1.0" + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/webpack/node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -8910,12 +8173,39 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, "@swc/core": { "optional": true }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, "esbuild": { "optional": true }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, "uglify-js": { "optional": true } @@ -8925,6 +8215,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -8935,29 +8226,11 @@ "node": ">= 8" } }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==" + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "license": "MIT" }, "node_modules/word-wrap": { "version": "1.2.5", @@ -9005,15 +8278,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -9026,7 +8290,8 @@ "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" }, "node_modules/yargs": { "version": "17.7.2", @@ -9073,12 +8338,37 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/src/OpenSEE/package.json b/src/OpenSEE/package.json index ccdc33fb..72015621 100644 --- a/src/OpenSEE/package.json +++ b/src/OpenSEE/package.json @@ -3,59 +3,66 @@ "name": "opensee", "private": true, "devDependencies": { + "@types/node": "^25.5.2", + "@types/d3": "7.0.0", + "@types/eonasdan-bootstrap-datetimepicker": "4.17.26", + "@types/flot": "0.0.31", + "@types/jquery": "3.5.6", + "@types/lodash": "4.14.172", + "@types/moment": "2.13.0", + "@types/query-string": "5.1.0", + "@types/react": "^17.0.19", + "@types/react-dom": "16.8.3", + "@types/react-router-dom": "4.3.1", + "@typescript-eslint/eslint-plugin": "^5.60.0", + "@typescript-eslint/parser": "^5.60.0", "css-loader": "6.2.0", + "eslint": "^8.43.0", + "eslint-plugin-react-hooks": "^7.0.1", "fork-ts-checker-webpack-plugin": "^9.0.2", - "node-polyfill-webpack-plugin": "1.1.3", "path": "0.12.7", "source-map-loader": "3.0.0", "style-loader": "3.2.1", - "terser-webpack-plugin": "5.1.3", + "terser-webpack-plugin": "5.3.17", "ts-loader": "9.2.5", "typescript": "5.5.3", "url-loader": "4.1.1", - "webpack": "^5.47.0", - "webpack-cli": "^4.7.2", - "@typescript-eslint/eslint-plugin": "^5.60.0", - "@typescript-eslint/parser": "^5.60.0", - "eslint": "^8.43.0" + "webpack": "5.106.2", + "webpack-cli": "^4.7.2" }, "dependencies": { - "@gpa-gemstone/react-forms": "1.1.75", - "@gpa-gemstone/react-interactive": "1.0.135", - "@gpa-gemstone/gpa-symbols": "0.0.43", + "@gpa-gemstone/application-typings": "0.0.98", + "@gpa-gemstone/common-pages": "0.0.180", + "@gpa-gemstone/gpa-symbols": "0.0.62", + "@gpa-gemstone/helper-functions": "0.0.60", + "@gpa-gemstone/react-forms": "1.1.123", + "@gpa-gemstone/react-graph": "1.0.109", + "@gpa-gemstone/react-interactive": "1.0.189", + "@gpa-gemstone/react-table": "1.2.113", + "@popperjs/core": "^2.11.8", "@reduxjs/toolkit": "1.8.3", - "@types/d3": "7.0.0", - "@types/eonasdan-bootstrap-datetimepicker": "4.17.26", - "@types/flot": "0.0.31", - "@types/jquery": "3.5.6", - "@types/lodash": "4.14.172", - "@types/moment": "2.13.0", - "@types/query-string": "5.1.0", - "@types/react": "^17.0.19", - "@types/react-dom": "16.8.3", - "@types/react-router-dom": "4.3.1", + "bootstrap": "4.6.2", "d3": "7.4.2", "history": "4.7.2", - "jquery": "3.7.1", "lodash": "^4.17.21", "moment": "2.30.1", "node": "^14.0.0", "query-string": "5.1.1", "raf": "3.4.0", - "react": "^18.0.2", "react-color": "2.19.3", + "react": "18.2.0", "react-dom": "18.2.0", "react-redux": "8.0.2", - "react-router-dom": "6.2.1", "reselect": "^4.0.0", - "styled-components": "^5.3.3", "typestyle": "2.0.1" }, "scripts": { "build": "npm prune && npm ci && webpack --mode=development", + "builddev": "npm install && webpack --mode=development", "buildrelease": "npm prune && npm ci && webpack --mode=production", "watch": "webpack --watch --color --mode=development", "update": "npx npm-check-updates", - "lint": "eslint . --ext .ts,.tsx" + "lint": "eslint . --ext .ts,.tsx", + "lint:fix": "eslint . --fix --ext .ts,.tsx" } -} +} \ No newline at end of file diff --git a/src/OpenSEE/packages.config b/src/OpenSEE/packages.config deleted file mode 100644 index b951020d..00000000 --- a/src/OpenSEE/packages.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/OpenSEE/tsconfig.json b/src/OpenSEE/tsconfig.json index ce886a01..088ceaf8 100644 --- a/src/OpenSEE/tsconfig.json +++ b/src/OpenSEE/tsconfig.json @@ -2,20 +2,24 @@ "compilerOptions": { "baseUrl": ".", "paths": { "*": [ "types/*" ] }, - "downlevelIteration": true, "jsx": "react", - "lib": [ "dom", "es5", "scripthost", "es2015", "es6" ], + "lib": [ "dom", "es5", "scripthost", "es2019", "es6", "es2020" ], "noEmitOnError": true, + "downlevelIteration": true, "noImplicitAny": false, "removeComments": true, "sourceMap": true, "target": "es5", + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "Node", + "skipLibCheck": true, + "strictNullChecks": true, "typeRoots": [ "node_modules/@types" ], - "esModuleInterop": true - //"module": "ESNext" }, + "include": [ "wwwroot/Scripts/TSX", "wwwroot/Scripts/TS/*" ], "exclude": [ - "node_modules" - ], - "include": [ "Scripts/TSX", "Scripts/TS/*" ] -} + "node_modules", + "lib" + ] +} \ No newline at end of file diff --git a/src/OpenSEE/webpack.config.js b/src/OpenSEE/webpack.config.js index 8bcba5c1..7743d129 100644 --- a/src/OpenSEE/webpack.config.js +++ b/src/OpenSEE/webpack.config.js @@ -1,8 +1,8 @@ "use strict"; const path = require("path"); -const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); +var webpack = require('webpack'); function buildConfig(env, argv) { if (env.NODE_ENV == undefined) env.NODE_ENV = 'development'; @@ -12,20 +12,10 @@ function buildConfig(env, argv) { context: path.resolve(__dirname), cache: true, entry: { - OpenSee: "./Scripts/TSX/OpenSee.tsx", - ToolTipDeltaWidget: "./Scripts/TSX/jQueryUI Widgets/TooltipWithDelta.tsx", - ToolTipWidget: "./Scripts/TSX/jQueryUI Widgets/Tooltip.tsx", - TimeCorrelatedSagsWidget: "./Scripts/TSX/jQueryUI Widgets/TimeCorrelatedSags.tsx", - PointWidget: "./Scripts/TSX/jQueryUI Widgets/AccumulatedPoints.tsx", - PhasorChartWidget: "./Scripts/TSX/jQueryUI Widgets/PhasorChart.tsx", - ScalarStatsWidget: "./Scripts/TSX/jQueryUI Widgets/ScalarStats.tsx", - LightningDataWidget: "./Scripts/TSX/jQueryUI Widgets/LightningData.tsx", - SettingsWidget: "./Scripts/TSX/jQueryUI Widgets/SettingWindow.tsx", - FFTTable: "./Scripts/TSX/jQueryUI Widgets/FFTTable.tsx", - HarmonicStatsWidget: "./Scripts/TSX/jQueryUI Widgets/HarmonicStats.tsx", + OpenSee: "./wwwroot/Scripts/TSX/OpenSee.tsx" }, output: { - path: path.resolve(__dirname, 'Scripts'), + path: path.resolve(__dirname, './wwwroot/Scripts'), filename: "[name].js", }, // Enable sourcemaps for debugging webpack's output. @@ -39,12 +29,11 @@ function buildConfig(env, argv) { // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'. { test: /\.tsx?$/, - include: path.resolve(__dirname, "Scripts"), + include: path.resolve(__dirname, 'wwwroot', "Scripts"), loader: "ts-loader", options: { transpileOnly: true } }, { test: /\.css$/, - include: path.resolve(__dirname, 'wwwroot', "Content"), use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], }, //{ @@ -63,8 +52,11 @@ function buildConfig(env, argv) { ], }, plugins: [ - new NodePolyfillPlugin(), - new ForkTsCheckerWebpackPlugin() + new ForkTsCheckerWebpackPlugin(), + new webpack.ProvidePlugin({ + $: "jquery", + "window.jQuery": "jquery", + }) ] }; diff --git a/src/OpenSEE/Content/bootstrap4-datetimepicker.css b/src/OpenSEE/wwwroot/Content/bootstrap4-datetimepicker.css similarity index 100% rename from src/OpenSEE/Content/bootstrap4-datetimepicker.css rename to src/OpenSEE/wwwroot/Content/bootstrap4-datetimepicker.css diff --git a/src/OpenSEE/Images/2-Line - 500.png b/src/OpenSEE/wwwroot/Images/2-Line - 500.png similarity index 100% rename from src/OpenSEE/Images/2-Line - 500.png rename to src/OpenSEE/wwwroot/Images/2-Line - 500.png diff --git a/src/OpenSEE/Images/openSEE - Waveform Viewer Header.png b/src/OpenSEE/wwwroot/Images/openSEE - Waveform Viewer Header.png similarity index 100% rename from src/OpenSEE/Images/openSEE - Waveform Viewer Header.png rename to src/OpenSEE/wwwroot/Images/openSEE - Waveform Viewer Header.png diff --git a/src/OpenSEE/wwwroot/Images/openSEE.png b/src/OpenSEE/wwwroot/Images/openSEE.png new file mode 100644 index 00000000..ff8bc1f8 Binary files /dev/null and b/src/OpenSEE/wwwroot/Images/openSEE.png differ diff --git a/src/OpenSEE/wwwroot/Images/openSEELogo.svg b/src/OpenSEE/wwwroot/Images/openSEELogo.svg new file mode 100644 index 00000000..83da6d8c --- /dev/null +++ b/src/OpenSEE/wwwroot/Images/openSEELogo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Components/AnalyticOptions.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Components/AnalyticOptions.tsx new file mode 100644 index 00000000..002a9726 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Components/AnalyticOptions.tsx @@ -0,0 +1,272 @@ +//****************************************************************************************************** +// RadioselectWindow.tsx - Gbtc +// +// Copyright � 2019, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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/13/2019 - Billy Ernest +// Generated original version of source code. +// 09/25/2019 - Christoph Lackner +// Added Settings Form +// +//****************************************************************************************************** +import * as React from 'react'; +import { OpenSee } from '../global'; +import { BtnDropdown } from "@gpa-gemstone/react-interactive"; +import { Select, Input } from "@gpa-gemstone/react-forms"; +import * as _ from 'lodash'; +import { GetDisplayLabel } from '../Graphs/Utils/Utilities'; +import AnalyticContext from '../Context/AnalyticContext'; +import EventContext from '../Context/EventContext'; +import { PlotStateStateContext } from '../Context/PlotStateContext'; +import { OverlappingStateContext } from '../Context/OverlappingContext'; +import { selectPlotKeys, selectEventIDs } from '../PlotSelectors'; +import { useAppSelector } from '../hooks'; +import { SelectSinglePlot } from '../Store/settingSlice'; +import { IPlotLifecycleActions } from '../Hooks/usePlotLifeCycle'; +import { BasePlots } from '../defaults'; + +interface IProps { + lifecycle: IPlotLifecycleActions; +} + +const options = { + order: [ + { Label: '1', Value: '1' }, + { Label: '2', Value: '2' }, + { Label: '3', Value: '3' }, + ], + trc: [ + { Label: '100', Value: '100' }, + { Label: '200', Value: '200' }, + { Label: '500', Value: '500' }, + ], + cycles: Array.from({ length: 15 }, (_, i) => ({ Label: `${i + 1}`, Value: `${i + 1}` })) +}; + +const AnalyticOptions = (props: IProps) => { + const [analytic, setAnalytic] = React.useContext(AnalyticContext); + const evt = React.useContext(EventContext); + const { meta } = React.useContext(PlotStateStateContext); + const overlapping = React.useContext(OverlappingStateContext); + const singlePlot = useAppSelector(SelectSinglePlot); + + const eventIDs = React.useMemo( + () => selectEventIDs(evt.Context.EventID, overlapping.events), + [evt.Context.EventID, overlapping.events] + ); + + const plotKeys = React.useMemo( + () => selectPlotKeys(meta, singlePlot), + [meta, singlePlot] + ); + + const defaultAnalyticBtns = [ + { Label: 'Fault Distance', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'FaultDistance', EventId: id })), DataType: 'FaultDistance' }, + { Label: 'FFT', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'FFT', EventId: id })), DataType: 'FFT' }, + { Label: 'First Derivative', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'FirstDerivative', EventId: id })), DataType: "FirstDerivative" }, + { Label: 'Fixed Clipped Waveforms', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'ClippedWaveforms', EventId: id })), DataType: 'ClippedWaveforms' }, + { Label: 'Frequency', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'Frequency', EventId: id })), DataType: 'Frequency' }, + { Label: 'High Pass Filter', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'HighPassFilter', EventId: id })), DataType: 'HighPassFilter' }, + { Label: 'Impedance', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'Impedance', EventId: id })), DataType: 'Impedance' }, + { Label: 'Low Pass Filter', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'LowPassFilter', EventId: id })), DataType: 'LowPassFilter' }, + { Label: 'Missing Voltage', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'MissingVoltage', EventId: id })), DataType: 'MissingVoltage' }, + { Label: 'Overlapping Waveform', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'OverlappingWave', EventId: id })), DataType: 'OverlappingWave' }, + { Label: 'Power', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'Power', EventId: id })), DataType: 'Power' }, + { Label: 'Rapid Voltage Change', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'RapidVoltage', EventId: id })), DataType: 'RapidVoltage' }, + { Label: 'Rectifier Output', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'Rectifier', EventId: id })), DataType: 'Rectifier' }, + { Label: 'Remove Current', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'RemoveCurrent', EventId: id })), DataType: 'RemoveCurrent' }, + { Label: 'Specified Harmonic', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'Harmonic', EventId: id })), DataType: 'Harmonic' }, + { Label: 'Symmetrical Components', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'SymetricComp', EventId: id })), DataType: 'SymetricComp' }, + { Label: 'THD', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'THD', EventId: id })), DataType: 'THD' }, + { Label: 'Unbalance', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'Unbalance', EventId: id })), DataType: 'Unbalance' }, + { Label: 'I2T', Callback: () => eventIDs.forEach(id => props.lifecycle.AddPlot({ DataType: 'I2T', EventId: id })), DataType: 'I2T' } + ]; + + const analyticBtns = React.useMemo(() => { + const activePlotTypes = plotKeys.map(key => key.DataType); + return defaultAnalyticBtns.filter(btn => !activePlotTypes.includes(btn.DataType as OpenSee.graphType)); + }, [plotKeys, eventIDs]); + + const analyticDebounce = (newAnalytic: OpenSee.IAnalyticContext) => { + setTimeout(() => { setAnalytic(newAnalytic); }, 500); + }; + + const renderPlotOptions = (key: OpenSee.IGraphProps) => { + if (BasePlots.includes(key.DataType)) + return null; + + if (key.DataType === "Harmonic") + return ( +
    +
    + Specified Harmonic +
    +
    + + Record={analytic} + Field={'Harmonic'} + Type={'integer'} + Setter={analyticDebounce} + Label={"Harmonic:"} + Valid={() => analytic.Harmonic != null} + Feedback="Harmonic value can not be empty" + /> +
    +
    + +
    +
    +
    +
    + ); + + if (key.DataType === "HighPassFilter") + return ( +
    +
    + High Pass Filter +
    +
    + + Record={analytic} + Field={'HPFOrder'} + Options={options.order} + Setter={setAnalytic} + Label={"Order:"} + /> +
    +
    + +
    +
    +
    +
    + ); + + if (key.DataType === "LowPassFilter") + return ( +
    +
    + Low Pass Filter +
    +
    + + Record={analytic} + Field={'LPFOrder'} + Options={options.order} + Setter={setAnalytic} + Label={"Order:"} + /> +
    +
    + +
    +
    +
    +
    + ); + + if (key.DataType === "Rectifier") + return ( +
    +
    + Rectifier Output +
    +
    + + Record={analytic} + Field={'Trc'} + Options={options.trc} + Setter={setAnalytic} + Label={"RC Time Const. (ms):"} + /> +
    +
    + +
    +
    +
    +
    + ); + + if (key.DataType === "FFT") + return ( +
    +
    + FFT +
    +
    + + Record={analytic} + Field={'FFTCycles'} + Setter={analyticDebounce} + Label={"Length(Cycles):"} + Valid={() => analytic.FFTCycles != null} + Feedback="FFT Cycles value can not be empty" + Type="integer" + /> +
    +
    + +
    +
    +
    +
    + ); + + return ( +
    +
    + {GetDisplayLabel(key.DataType)} +
    +
    + +
    +
    +
    +
    + ); + }; + + return ( +
    +
    + {analyticBtns.length > 0 && ( +
    + +
    + )} + {_.uniqBy(plotKeys, "DataType").map(renderPlotOptions)} +
    +
    + ); +}; + +export default AnalyticOptions; diff --git a/src/OpenSEE/Scripts/TSX/Components/OverlappingEvents.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Components/OverlappingEvents.tsx similarity index 55% rename from src/OpenSEE/Scripts/TSX/Components/OverlappingEvents.tsx rename to src/OpenSEE/wwwroot/Scripts/TSX/Components/OverlappingEvents.tsx index 86a7f656..2393175e 100644 --- a/src/OpenSEE/Scripts/TSX/Components/OverlappingEvents.tsx +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Components/OverlappingEvents.tsx @@ -23,30 +23,51 @@ // Fix issue where events weren't getting grouped by meter // //****************************************************************************************************** - - import React from 'react'; import { useAppDispatch, useAppSelector } from '../hooks'; -import { SelectEventListLoading, SelectEventList, EnableOverlappingEvent } from '../store/overlappingEventsSlice'; -import { SelectSinglePlot, EnableSinglePlot, SelectUseOverlappingTime, SetUseOverlappingTime, SelectTimeUnit } from '../store/settingSlice'; -import { CheckBox } from '@gpa-gemstone/react-forms'; +import { SelectSinglePlot, SetSinglePlot, SelectUseOverlappingTime, SetUseOverlappingTime, SelectTimeUnit } from '../Store/settingSlice'; +import { CheckBox, RadioButtons } from '@gpa-gemstone/react-forms'; import _ from 'lodash'; import { LoadingIcon } from '../Graphs/ChartIcons'; -import { defaultSettings } from '../defaults'; +import { Alert } from '@gpa-gemstone/react-interactive'; +import { TimeUnitOptions } from '../defaults'; +import { OverlappingStateContext } from '../Context/OverlappingContext'; + +interface IProps { + EnableOverlappingEvent: (eventId: number) => void; +} + +type OverlappingTimeMode = 'Original' | 'Selected'; -const OverlappingEventWindow = () => { - const eventList = useAppSelector(SelectEventList); - const groupedEvents = _.groupBy(eventList, 'MeterName'); +interface IOverlappingTimeModeRecord { + mode: OverlappingTimeMode +} + +const overlappingTimeOptions: Array<{ Label: string, Value: OverlappingTimeMode }> = [ + { Label: "Relative to Original Event", Value: 'Original' }, + { Label: "Relative to Selected Event", Value: 'Selected' } +]; + +const OverlappingEventWindow = (props: IProps) => { + const dispatch = useAppDispatch(); const singlePlot = useAppSelector(SelectSinglePlot); - const eventListLoading = useAppSelector(SelectEventListLoading); const useOverlappingTime = useAppSelector(SelectUseOverlappingTime); const timeUnit = useAppSelector(SelectTimeUnit); - const dispatch = useAppDispatch(); + const overlapping = React.useContext(OverlappingStateContext); + const groupedEvents = _.groupBy(overlapping.events, 'MeterName'); return (
    - {eventListLoading ? ( + {overlapping.loading === 'Error' ? ( +
    +
    + + Error retrieving overlapping events. + +
    +
    + ) : overlapping.loading !== 'Idle' ? ( ) : (
    @@ -56,25 +77,26 @@ const OverlappingEventWindow = () => { dispatch(EnableSinglePlot(item.singlePlot))} + Setter={(item) => dispatch(SetSinglePlot(item.singlePlot))} Label={"Display all events on same plot"} Help={"Draws the waveform groups (e.g., Voltage) for the current event and the selected asset(s) on a single plot for comparison."} />
    - {defaultSettings.TimeUnit.options[timeUnit.current].short.includes('since') && - (!singlePlot || (singlePlot && !eventList.some(i => i.Selected))) ? -
    -
    - dispatch(SetUseOverlappingTime(!e.target.checked))} /> - -
    -
    - dispatch(SetUseOverlappingTime(e.target.checked))} /> - -
    -
    - : null} + {TimeUnitOptions[timeUnit.current].short.includes('since') && + (!singlePlot || (singlePlot && !overlapping.events.some(i => i.Selected))) ? +
    + + Record={{ mode: useOverlappingTime ? 'Selected' : 'Original' }} + Field="mode" + Setter={(record) => dispatch(SetUseOverlappingTime(record.mode === 'Selected'))} + Label="" + Position="horizontal" + Style={{ marginBottom: 0 }} + Options={overlappingTimeOptions} + /> +
    + : null} {Object.entries(groupedEvents).map(([meterName, events]) => ( @@ -82,15 +104,15 @@ const OverlappingEventWindow = () => { {meterName} {events.map((event, idx) => (
    -
    - dispatch(EnableOverlappingEvent(updatedEvent.EventID))} - Label={event.AssetName} - /> -
    +
    + props.EnableOverlappingEvent(updatedEvent.EventID)} + Label={event.AssetName} + />
    +
    ))} ))} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Context/AnalyticContext.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Context/AnalyticContext.tsx new file mode 100644 index 00000000..1e7eb399 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Context/AnalyticContext.tsx @@ -0,0 +1,67 @@ +//****************************************************************************************************** +// AnalyticContext.tsx - 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/11/2026 - G. Santos +// Migrated code from slice to context. +// +//****************************************************************************************************** +import * as React from 'react'; +import { OpenSee } from '../global'; + +const DefaultContext: OpenSee.IAnalyticContext = { + Harmonic: 1, + LPFOrder: 2, + HPFOrder: 2, + Trc: 500, + FFTCycles: 1, + FFTStartTime: 0 +}; + +type IAnalyticContextType = [OpenSee.IAnalyticContext, React.Dispatch>]; + +export const AnalyticContext = React.createContext([DefaultContext, () => { }]); + +// This likely can and SHOULD be converted to a simple state in the topmost element... +export const AnalyticProvider = (props: React.PropsWithChildren<{}>) => { + const analytic = React.useState(DefaultContext); + + return ( + + {props.children} + + ); +}; + +export const SelectAnalyticOptions = (context: OpenSee.IAnalyticContext, key: OpenSee.graphType) => { + switch (key) { + case 'LowPassFilter': + return [context.LPFOrder]; + case 'HighPassFilter': + return [context.HPFOrder]; + case 'Harmonic': + return [context.Harmonic]; + case 'Rectifier': + return [context.Trc]; + case 'FFT': + return [context.FFTCycles, context.FFTStartTime]; + default: + return []; + } +} + +export default AnalyticContext; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Context/EventContext.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Context/EventContext.tsx new file mode 100644 index 00000000..9c4238c9 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Context/EventContext.tsx @@ -0,0 +1,133 @@ +//****************************************************************************************************** +// EventContext.tsx - 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/06/2026 - G. Santos +// Generated original version of source code. +// +//****************************************************************************************************** +import * as React from 'react'; +import { OpenSee } from '../global'; +import { Application } from '@gpa-gemstone/application-typings'; + +interface IContextSettings { + EventID: number, + BreakerOperation?: string +} + +interface IDispatchFunctions { + SettingsDispatch: React.Dispatch> +} + +interface IEventContextState { + EventID: number, + EventInfo: OpenSee.IEventInfo | null, + LookupInfo: OpenSee.INextBackLookup | null, + Status: Application.Types.Status +} + +const defaultState: IEventContextState = { + EventID: -1, + EventInfo: null, + LookupInfo: null, + Status: 'uninitiated' +}; + +interface IEventContext { + Context: IEventContextState, + Dispatch: React.MutableRefObject, +} + +const defaultDispatch: IDispatchFunctions = { + SettingsDispatch: () => { /* noop */ } +} + +const defaultEventState: IEventContextState = { + EventID: -1, + EventInfo: null, + LookupInfo: null, + Status: 'uninitiated' +} + +export const EventContext = React.createContext({ Context: defaultState, Dispatch: { current: defaultDispatch } }); + +export const EventProvider = (props: React.PropsWithChildren<{}>) => { + const [contextState, setContextState] = React.useState(defaultState); + const [settings, setSettings] = React.useState(defaultEventState); + + const functionRef = React.useRef(defaultDispatch); + functionRef.current = { SettingsDispatch: setSettings }; + + const context = React.useMemo(() => ({ Dispatch: functionRef, Context: contextState }), [contextState]); + + React.useEffect(() => { + if (settings == null || settings.EventID == null || isNaN(settings.EventID) || settings.EventID < 0) + return; + + setContextState(state => ({ + ...state, + EventID: settings.EventID, + Status: 'loading' + })); + + const eventHandle = $.ajax({ + type: "GET", + url: `${homePath}api/OpenSEE/GetHeaderData` + + `?eventId=${settings.EventID}` + + `${settings.BreakerOperation != null ? "&breakeroperation=" + settings.BreakerOperation : ""}`, + dataType: 'json', + cache: true, + async: true + }); + + const lookupHandle = $.ajax({ + type: "GET", + url: `${homePath}api/OpenSEE/GetNavData?eventId=${settings.EventID}`, + dataType: 'json', + cache: true, + async: true + }); + + Promise.all([eventHandle, lookupHandle]).then(([evtResult, lookupResult]) => { + setContextState({ + Status: 'idle', + EventID: settings.EventID, + EventInfo: evtResult, + LookupInfo: lookupResult + }); + }, () => { + console.error("Unable to load event context data."); + setContextState(state => ({ + ...state, + Status: 'error' + })); + }); + + return () => { + if (eventHandle?.abort != null) eventHandle.abort(); + if (lookupHandle?.abort != null) lookupHandle.abort(); + } + }, [settings?.EventID, settings?.BreakerOperation]); + + return ( + + {props.children} + + ); +}; + +export default EventContext; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Context/HoverContext.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Context/HoverContext.tsx new file mode 100644 index 00000000..141efec0 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Context/HoverContext.tsx @@ -0,0 +1,39 @@ +//****************************************************************************************************** +// HoverContext.tsx - 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/06/2026 - G. Santos +// Merged provider and context code into one document. +// +//****************************************************************************************************** +import * as React from 'react'; + +type HoverContextType = [[number, number], React.Dispatch>]; + +// ToDo: This likely can and SHOULD be converted to a simple state in the topmost element... +export const HoverContext = React.createContext([[0, 0], () => { }]); +export const HoverProvider = (props: React.PropsWithChildren<{}>) => { + const hover = React.useState<[number, number]>([0, 0]); + + return ( + + {props.children} + + ); +}; + +export default HoverContext; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Context/OverlappingContext.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Context/OverlappingContext.tsx new file mode 100644 index 00000000..f027a765 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Context/OverlappingContext.tsx @@ -0,0 +1,132 @@ +// OverlappingContext.tsx +// Manages the list of overlapping events and their selection state. +// Isolated from plot data/meta -- only two components consume this. + +import * as React from 'react'; +import { OpenSee } from '../global'; +import { getOverlappingEvents } from '../Data/GraphLogic'; + +interface IOverlappingState { + loading: OpenSee.LoadingState; + events: OpenSee.OverlappingEvents[]; +} + +interface IOverlappingActions { + LoadOverlappingEvents: (eventId: number) => void; + ToggleEventSelection: (eventId: number) => void; + SetSelectedEvents: (eventIds: number[]) => void; + // Called externally after adding plot data for an overlapping event + SetEventSelected: (eventId: number, selected: boolean) => void; +} + +const defaultState: IOverlappingState = { + loading: 'Uninitiated', + events: [] +}; + +const defaultActions: IOverlappingActions = { + LoadOverlappingEvents: () => { /* noop */ }, + ToggleEventSelection: () => { /* noop */ }, + SetSelectedEvents: () => { /* noop */ }, + SetEventSelected: () => { /* noop */ }, +}; + +export const OverlappingStateContext = React.createContext(defaultState); +export const OverlappingActionContext = React.createContext(defaultActions); + +export const OverlappingProvider = (props: React.PropsWithChildren<{}>) => { + const [state, setState] = React.useState(defaultState); + const selectedEventIdsRef = React.useRef>(new Set()); + + const actions = React.useMemo(() => ({ + LoadOverlappingEvents: (eventId) => { + if (eventId == null || isNaN(eventId) || eventId <= 0) return; + + setState(prev => ({ ...prev, loading: 'Loading' })); + + const handle = getOverlappingEvents(eventId, null, null); + handle.then( + (data) => { + setState(prev => { + const newEvents = [...prev.events]; + data.forEach((event: any) => { + const idx = newEvents.findIndex(e => e.EventID === event.EventID); + const parsed: OpenSee.OverlappingEvents = { + Selected: selectedEventIdsRef.current.has(event.EventID) || (idx >= 0 ? newEvents[idx].Selected : false), + AssetName: event.AssetName, + MeterName: event.MeterName, + EventID: event.EventID, + StartTime: new Date(event.StartTime + 'Z').getTime(), + EndTime: new Date(event.EndTime + 'Z').getTime(), + EventType: event.EventType, + Inception: event.Inception, + DurationEndTime: event.DurationEndTime + }; + if (idx < 0) + newEvents.push(parsed); + else + newEvents[idx] = { ...newEvents[idx], ...parsed }; + }); + return { loading: 'Idle', events: newEvents }; + }); + }, + () => { + setState(prev => ({ ...prev, loading: 'Error' })); + } + ); + }, + + ToggleEventSelection: (eventId) => { + setState(prev => { + const idx = prev.events.findIndex(e => e.EventID === eventId); + if (idx < 0) return prev; + + const newEvents = [...prev.events]; + const selected = !newEvents[idx].Selected; + if (selected) + selectedEventIdsRef.current.add(eventId); + else + selectedEventIdsRef.current.delete(eventId); + + newEvents[idx] = { ...newEvents[idx], Selected: selected }; + return { ...prev, events: newEvents }; + }); + }, + + SetSelectedEvents: (eventIds) => { + selectedEventIdsRef.current = new Set(eventIds); + setState(prev => { + const newEvents = prev.events.map(event => ({ + ...event, + Selected: selectedEventIdsRef.current.has(event.EventID) + })); + + return { ...prev, events: newEvents }; + }); + }, + + SetEventSelected: (eventId, selected) => { + if (selected) + selectedEventIdsRef.current.add(eventId); + else + selectedEventIdsRef.current.delete(eventId); + + setState(prev => { + const idx = prev.events.findIndex(e => e.EventID === eventId); + if (idx < 0) return prev; + + const newEvents = [...prev.events]; + newEvents[idx] = { ...newEvents[idx], Selected: selected }; + return { ...prev, events: newEvents }; + }); + } + }), []); + + return ( + + + {props.children} + + + ); +}; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotDataContext.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotDataContext.tsx new file mode 100644 index 00000000..c2a7bb5a --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotDataContext.tsx @@ -0,0 +1,135 @@ +// PlotDataContext.tsx +// Stores the heavy iD3DataSeries arrays keyed by plot. +// This context only updates on data fetch, plot removal, and analytic refresh. +// Interactive actions (zoom, enable trace, etc.) never touch this context. + +import * as React from 'react'; +import { OpenSee } from '../global'; +import { PlotKey, toPlotKey } from './PlotKeys'; + +interface IPlotDataState { + plots: Record; +} + +interface IPlotDataActions { + // Append new series to an existing plot (used during initial fetch) + AppendPlotData: (key: OpenSee.IGraphProps, data: OpenSee.iD3DataSeries[], eventId: number) => void; + // Remove a plot entirely + RemovePlotData: (key: OpenSee.IGraphProps) => void; + // Remove only series belonging to a specific event (for overlapping removal) + FilterPlotDataByEvent: (key: OpenSee.IGraphProps, eventId: number) => void; + // Clear series but keep the key present (for analytic refresh) + ClearPlotData: (key: OpenSee.IGraphProps) => void; + // Initialize an empty entry so meta can be created in parallel + InitPlotData: (key: OpenSee.IGraphProps) => void; + // Replace individual series with high-res versions, matched by legend fields. + // Preserves EventID from the original series. + ReplaceDetailedData: (key: OpenSee.IGraphProps, detailed: OpenSee.iD3DataSeries[]) => void; +} + +const defaultState: IPlotDataState = { plots: {} }; + +const defaultActions: IPlotDataActions = { + AppendPlotData: () => { /* noop */ }, + RemovePlotData: () => { /* noop */ }, + FilterPlotDataByEvent: () => { /* noop */ }, + ClearPlotData: () => { /* noop */ }, + InitPlotData: () => { /* noop */ }, + ReplaceDetailedData: () => { /* noop */ }, +}; + +export const PlotDataStateContext = React.createContext(defaultState); +export const PlotDataActionContext = React.createContext(defaultActions); + +export const PlotDataProvider = (props: React.PropsWithChildren<{}>) => { + const [state, setState] = React.useState(defaultState); + + const actions = React.useMemo(() => ({ + InitPlotData: (key) => { + const pk = toPlotKey(key); + setState(prev => { + if (prev.plots[pk] != null) + return prev; + return { plots: { ...prev.plots, [pk]: [] } }; + }); + }, + + AppendPlotData: (key, data, eventId) => { + const pk = toPlotKey(key); + setState(prev => { + const existing = prev.plots[pk] ?? []; + // Stamp each incoming series with the source event ID + const stamped = data.map(d => ({ ...d, EventID: eventId })); + return { + plots: { ...prev.plots, [pk]: [...existing, ...stamped] } + }; + }); + }, + + RemovePlotData: (key) => { + const pk = toPlotKey(key); + setState(prev => { + const next = { ...prev.plots }; + delete next[pk]; + return { plots: next }; + }); + }, + + FilterPlotDataByEvent: (key, eventId) => { + const pk = toPlotKey(key); + setState(prev => { + const existing = prev.plots[pk]; + if (!existing) return prev; + return { + plots: { + ...prev.plots, + [pk]: existing.filter(d => d.EventID !== eventId) + } + }; + }); + }, + + ClearPlotData: (key) => { + const pk = toPlotKey(key); + setState(prev => ({ + plots: { ...prev.plots, [pk]: [] } + })); + }, + + ReplaceDetailedData: (key, detailed) => { + const pk = toPlotKey(key); + setState(prev => { + const existing = prev.plots[pk]; + if (!existing || detailed.length === 0) return prev; + + const updated = [...existing]; + const matched: number[] = []; + + detailed.forEach(d => { + const idx = updated.findIndex((od, i) => + od.LegendGroup === d.LegendGroup && + od.LegendHorizontal === d.LegendHorizontal && + od.LegendVertical === d.LegendVertical && + od.LegendVGroup === d.LegendVGroup && + !matched.includes(i) + ); + if (idx >= 0) { + // Preserve EventID from the original series + updated[idx] = { ...d, EventID: existing[idx].EventID }; + matched.push(idx); + } + }); + + return { plots: { ...prev.plots, [pk]: updated } }; + }); + }, + }), []); + + return ( + + + {props.children} + + + ); +}; \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotKeys.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotKeys.tsx new file mode 100644 index 00000000..738dd51a --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotKeys.tsx @@ -0,0 +1,76 @@ +// PlotKeys.ts +// Stable string keys for identifying plots and individual series in Record-based maps. +// Used across PlotDataContext and PlotStateContext to avoid object reference lookups. +// +// PlotKey identifies a plot (DataType + EventId). +// SeriesKey identifies a single trace within a plot. It uses explicit fields +// so that neither context needs to depend on iD3DataSeries directly. + +import { OpenSee } from '../global'; + +export type PlotKey = string; + +export function toPlotKey(key: OpenSee.IGraphProps): PlotKey { + return `${key.DataType}|${key.EventId}`; +} + +export function fromPlotKey(plotKey: PlotKey): OpenSee.IGraphProps { + const [DataType, id] = plotKey.split('|'); + return { DataType: DataType as OpenSee.graphType, EventId: parseInt(id, 10) }; +} + +export type SeriesKey = string; +export type LegendTraceKey = string; + +// Builds a stable key from the four legend dimensions plus event ID. +// Callers pull these fields from whatever source they have access to -- +// chart components read them from data series, the lifecycle hook reads +// them at ingestion time. PlotStateContext never needs to know where +// the values came from. +export function toSeriesKey( + legendGroup: string, + legendHorizontal: string, + legendVertical: string, + legendVGroup: string, + eventId: number +): SeriesKey { + return `${legendGroup}|${legendHorizontal}|${legendVertical}|${legendVGroup}|${eventId}`; +} + +export function toLegendTraceKey( + legendHorizontal: string, + legendVertical: string, + legendVGroup: string +): LegendTraceKey { + return `${legendHorizontal}|${legendVertical}|${legendVGroup}`; +} + +// Convenience overload for callers that already have a series-shaped object. +// Keeps chart components concise without forcing PlotStateContext to import the data type. +export function seriesToKey(series: { + LegendGroup: string; + LegendHorizontal: string; + LegendVertical: string; + LegendVGroup: string; + EventID: number; +}): SeriesKey { + return toSeriesKey( + series.LegendGroup, + series.LegendHorizontal, + series.LegendVertical, + series.LegendVGroup, + series.EventID + ); +} + +export function seriesToLegendTraceKey(series: { + LegendHorizontal: string; + LegendVertical: string; + LegendVGroup: string; +}): LegendTraceKey { + return toLegendTraceKey( + series.LegendHorizontal, + series.LegendVertical, + series.LegendVGroup + ); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotStateContext.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotStateContext.tsx new file mode 100644 index 00000000..1a592ef2 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotStateContext.tsx @@ -0,0 +1,670 @@ +// PlotStateContext.tsx +// Holds all lightweight, frequently-changing plot state: per-plot metadata +// (enabled flags, axis limits, zoom state, loading state) and time boundaries. +// +// This context has ZERO knowledge of PlotDataContext. Any action that needs +// DataPoints for limit recomputation receives the data as a parameter from +// the caller, who has access to both contexts. + +import * as React from 'react'; +import _ from 'lodash'; +import { OpenSee } from '../global'; +import { defaultSettings } from '../defaults'; +import { PlotKey, SeriesKey, LegendTraceKey, toPlotKey, seriesToKey } from './PlotKeys'; +import { + IPlotMeta, + createEmptyMeta, + recomputeDataLimits, + recomputeNonAutoLimits, + scaleLimits, + scaleLimitsByFactor, + updateAutoLimits, + updateActiveUnits, + getPrimaryAxis, + getLocalUnitSettings, + saveUnitSettings, + getIndex, + getLegendSelectionsFromEnabled, + getEnabledFromLegendSelections +} from './PlotStateUtilities'; + +export type PlotDataMap = Record; + +export interface IPlotStateState { + startTime: number; + endTime: number; + fftLimits: [number, number]; + cycleLimits: [number, number]; + meta: Record; +} + +// Actions that need DataPoints for recomputation take a data param. +export interface IPlotStateActions { + SetTimeLimit: (start: number, end: number, plotData: PlotDataMap) => void; + SetCycleLimit: (start: number, end: number, plotData: PlotDataMap) => void; + SetFFTLimits: (start: number, end: number, plotData: PlotDataMap) => void; + ResetZoom: (start: number, end: number, plotData: PlotDataMap) => void; + SetZoomedLimits: (limits: [number, number], key: OpenSee.IGraphProps, plotData: PlotDataMap) => void; + SetUnit: (unit: OpenSee.Unit, value: number, auto: boolean, key: OpenSee.IGraphProps, plotData: PlotDataMap) => void; + EnableTrace: (key: OpenSee.IGraphProps, traces: SeriesKey[], enabled: boolean, data: OpenSee.iD3DataSeries[]) => void; + SetLegendSelections: (key: OpenSee.IGraphProps, data: OpenSee.iD3DataSeries[], selectedAssets: string[], selectedTraces: LegendTraceKey[]) => void; + SetIsManual: (key: OpenSee.IGraphProps, unit: OpenSee.Unit, manual: boolean) => void; + SetManualLimits: (limits: [number, number], key: OpenSee.IGraphProps, axis: OpenSee.Unit, auto: boolean, data: OpenSee.iD3DataSeries[], factor?: number) => void; + SetSelectPoint: (time: number, plotData: PlotDataMap) => void; + ClearSelectPoints: () => void; + RemoveSelectPoints: (index: number) => void; + InitPlotMeta: (key: OpenSee.IGraphProps, yLimits?: OpenSee.IUnitCollection, isZoomed?: boolean) => void; + RemovePlotMeta: (key: OpenSee.IGraphProps) => void; + SetPlotLoading: (key: OpenSee.IGraphProps, loading: OpenSee.LoadingState) => void; + OnDataAppended: (key: OpenSee.IGraphProps, data: OpenSee.iD3DataSeries[], enabled: Record) => void; +} + +// Defaults +const defaultState: IPlotStateState = { + startTime: 0, + endTime: 0, + fftLimits: [0, 0], + cycleLimits: [0, 1000.0 / 60.0], + meta: {} +}; + +const defaultActions: IPlotStateActions = { + SetTimeLimit: () => { /* noop */ }, + SetCycleLimit: () => { /* noop */ }, + SetFFTLimits: () => { /* noop */ }, + ResetZoom: () => { /* noop */ }, + SetZoomedLimits: () => { /* noop */ }, + SetUnit: () => { /* noop */ }, + EnableTrace: () => { /* noop */ }, + SetLegendSelections: () => { /* noop */ }, + SetIsManual: () => { /* noop */ }, + SetManualLimits: () => { /* noop */ }, + SetSelectPoint: () => { /* noop */ }, + ClearSelectPoints: () => { /* noop */ }, + RemoveSelectPoints: () => { /* noop */ }, + InitPlotMeta: () => { /* noop */ }, + RemovePlotMeta: () => { /* noop */ }, + SetPlotLoading: () => { /* noop */ }, + OnDataAppended: () => { /* noop */ }, +}; + +// Contexts +export const PlotStateStateContext = React.createContext(defaultState); +export const PlotStateActionContext = React.createContext(defaultActions); + +// Provider + +export const PlotStateProvider = (props: React.PropsWithChildren<{}>) => { + const [state, setState] = React.useState(defaultState); + + const actions = React.useMemo(() => ({ + SetTimeLimit: (start, end, plotData) => { + if (Math.abs(start - end) < 10) return; + setState(prev => { + const newMeta = { ...prev.meta }; + Object.keys(newMeta).forEach(pk => { + const m = newMeta[pk]; + const d = plotData[pk] ?? []; + const [s, e] = m.key.DataType === 'FFT' + ? prev.fftLimits + : m.key.DataType === 'OverlappingWave' + ? prev.cycleLimits + : [start, end]; + newMeta[pk] = { ...m, yLimits: updateAutoLimits(m, d, s, e) }; + }); + return { ...prev, startTime: start, endTime: end, meta: newMeta }; + }); + }, + + SetCycleLimit: (start, end, plotData) => { + if (Math.abs(start - end) < 5) return; + setState(prev => { + const newMeta = { ...prev.meta }; + Object.keys(newMeta).forEach(pk => { + const m = newMeta[pk]; + if (m.key.DataType !== 'OverlappingWave') return; + const d = plotData[pk] ?? []; + newMeta[pk] = { ...m, yLimits: updateAutoLimits(m, d, start, end) }; + }); + return { ...prev, cycleLimits: [start, end], meta: newMeta }; + }); + }, + + SetFFTLimits: (start, end, plotData) => { + if (Math.abs(start - end) < 1) return; + setState(prev => { + const newMeta = { ...prev.meta }; + Object.keys(newMeta).forEach(pk => { + const m = newMeta[pk]; + if (m.key.DataType !== 'FFT') return; + const d = plotData[pk] ?? []; + newMeta[pk] = { ...m, yLimits: updateAutoLimits(m, d, start, end) }; + }); + return { ...prev, fftLimits: [start, end], meta: newMeta }; + }); + }, + + ResetZoom: (start, end, plotData) => { + setState(prev => { + if (Math.abs(start - end) < 10) return prev; + const newMeta = { ...prev.meta }; + let newFftLimits = prev.fftLimits; + let newCycleLimits = prev.cycleLimits; + + Object.keys(newMeta).forEach(pk => { + const m = newMeta[pk]; + const d = plotData[pk] ?? []; + + if (m.key.DataType === 'FFT' && d.length > 0) { + const xMin = Math.min(...d.map(s => Math.min(...s.DataPoints.map(pt => pt[0])))); + const xMax = Math.max(...d.map(s => Math.max(...s.DataPoints.map(pt => pt[0])))); + newFftLimits = [xMin, xMax]; + } + + if (m.key.DataType === 'OverlappingWave' && d.length > 0) { + const xMin = Math.min(...d.map(s => Math.min(...s.DataPoints.map(pt => pt[0]).filter(v => !isNaN(v))))); + const xMax = Math.max(...d.map(s => Math.max(...s.DataPoints.map(pt => pt[0]).filter(v => !isNaN(v))))); + newCycleLimits = [xMin, xMax]; + } + + const newLimits = { ...m.yLimits }; + const axes = _.uniq(d.map(s => s.Unit)); + axes.forEach(axis => { + newLimits[axis] = { ...newLimits[axis], zoomedLimits: [0, 1] as [number, number] }; + }); + newMeta[pk] = { ...m, isZoomed: false, yLimits: newLimits }; + }); + + Object.keys(newMeta).forEach(pk => { + const m = newMeta[pk]; + const d = plotData[pk] ?? []; + const [s, e] = m.key.DataType === 'FFT' + ? newFftLimits + : m.key.DataType === 'OverlappingWave' + ? newCycleLimits + : [start, end]; + newMeta[pk] = { ...m, yLimits: updateAutoLimits(m, d, s, e) }; + }); + + return { + ...prev, + startTime: start, + endTime: end, + fftLimits: newFftLimits, + cycleLimits: newCycleLimits, + meta: newMeta + }; + }); + }, + + SetZoomedLimits: (limits, key, plotData) => { + const pk = toPlotKey(key); + setState(prev => { + const meta = prev.meta[pk]; + if (meta == null) return prev; + + const getActiveLimits = (meta: IPlotMeta, axis: OpenSee.Unit): [number, number] => { + if (meta.yLimits[axis].isManual) + return meta.yLimits[axis].manualLimits; + if (meta.isZoomed) + return meta.yLimits[axis].zoomedLimits; + return meta.yLimits[axis].dataLimits; + }; + + const primaryAxis = getPrimaryAxis(meta.key); + const oldLimits = getActiveLimits(meta, primaryAxis); + const newLimits = { ...meta.yLimits }; + const data = plotData[pk] ?? []; + const enabledAxes = _.uniq( + data.filter(s => meta.enabled[seriesToKey(s)]).map(s => s.Unit) + ); + + enabledAxes.forEach(axis => { + if (axis === primaryAxis) { + newLimits[axis] = { ...newLimits[axis], zoomedLimits: limits }; + } else { + const current = getActiveLimits(meta, axis); + newLimits[axis] = { + ...newLimits[axis], + zoomedLimits: recomputeNonAutoLimits(oldLimits, limits, current) + }; + } + }); + + return { + ...prev, + meta: { + ...prev.meta, + [pk]: { ...meta, isZoomed: true, yLimits: newLimits } + } + }; + }); + }, + + SetUnit: (unit, value, auto, key, plotData) => { + setState(prev => { + const newMeta = { ...prev.meta }; + + Object.keys(newMeta).forEach(pk => { + const m = newMeta[pk]; + if (m.key.DataType !== key.DataType) return; + const d = plotData[pk] ?? []; + + const oldUnitIndex = m.yLimits[unit].current; + const unitSetting: OpenSee.IUnitSetting = defaultSettings.Units[unit]; + const oldFactor = unitSetting.options?.[oldUnitIndex]?.factor; + const newFactor = unitSetting.options?.[value]?.factor; + const isPU = oldFactor === undefined || newFactor === undefined; + + const newLimits = { ...m.yLimits }; + newLimits[unit] = { ...newLimits[unit], isAuto: auto, current: value }; + + const filteredData = d.filter(s => m.enabled[seriesToKey(s)] && s.Unit === unit); + const autoIdx = updateActiveUnits(newLimits, unit, filteredData, prev.startTime, prev.endTime, null); + let newUnitIndex = value; + if (autoIdx >= 0) { + newLimits[unit] = { ...newLimits[unit], current: autoIdx }; + newUnitIndex = autoIdx; + } + + const [s, e] = m.key.DataType === 'FFT' + ? prev.fftLimits + : m.key.DataType === 'OverlappingWave' + ? prev.cycleLimits + : [prev.startTime, prev.endTime]; + + const oldDataLimits = newLimits[unit].dataLimits; + const computed = recomputeDataLimits(s, e, filteredData, newLimits[unit].current); + newLimits[unit] = { ...newLimits[unit], dataLimits: computed }; + + if (isPU) { + newLimits[unit] = { + ...newLimits[unit], + zoomedLimits: scaleLimits(oldDataLimits, computed, newLimits[unit].zoomedLimits), + manualLimits: scaleLimits(oldDataLimits, computed, newLimits[unit].manualLimits) + }; + } else { + newLimits[unit] = { + ...newLimits[unit], + manualLimits: scaleLimitsByFactor(oldUnitIndex, newUnitIndex, unit, newLimits[unit].manualLimits), + zoomedLimits: scaleLimitsByFactor(oldUnitIndex, newUnitIndex, unit, newLimits[unit].zoomedLimits) + }; + } + + newMeta[pk] = { ...m, yLimits: newLimits }; + }); + + saveUnitSettings(newMeta, plotData); + return { ...prev, meta: newMeta }; + }); + }, + + EnableTrace: (key, traces, enabled, data) => { + const pk = toPlotKey(key); + setState(prev => { + const meta = prev.meta[pk]; + if (!meta) return prev; + + const newEnabled = { ...meta.enabled }; + traces.forEach(sk => { newEnabled[sk] = enabled; }); + const selections = getLegendSelectionsFromEnabled(data, newEnabled); + + // Figure out which axes were affected by looking up the toggled series + const affectedAxes = _.uniq( + data.filter(s => traces.includes(seriesToKey(s))).map(s => s.Unit).filter(unit => unit != null) + ); + const newLimits = { ...meta.yLimits }; + const primaryAxis = getPrimaryAxis(key); + const [s, e] = meta.key.DataType === 'FFT' + ? prev.fftLimits + : meta.key.DataType === 'OverlappingWave' + ? prev.cycleLimits + : [prev.startTime, prev.endTime]; + + affectedAxes.forEach(axis => { + const relevantData = data.filter(item => newEnabled[seriesToKey(item)] && item.Unit === axis); + const recomputed = recomputeDataLimits(s, e, relevantData, meta.yLimits[axis].current); + newLimits[axis] = { ...newLimits[axis], dataLimits: recomputed }; + newLimits[axis] = { + ...newLimits[axis], + zoomedLimits: recomputeNonAutoLimits( + meta.yLimits[primaryAxis].dataLimits, + meta.yLimits[primaryAxis].zoomedLimits, + recomputed + ) + }; + updateActiveUnits(newLimits, axis, relevantData, prev.startTime, prev.endTime, null); + }); + + return { + ...prev, + meta: { + ...prev.meta, + [pk]: { + ...meta, + enabled: newEnabled, + selectedAssets: selections.selectedAssets, + selectedTraces: selections.selectedTraces, + legendSelectionsUserSet: true, + yLimits: newLimits + } + } + }; + }); + }, + + SetLegendSelections: (key, data, selectedAssets, selectedTraces) => { + const pk = toPlotKey(key); + setState(prev => { + const meta = prev.meta[pk]; + if (!meta) return prev; + + const newEnabled = getEnabledFromLegendSelections(data, selectedAssets, selectedTraces); + const affectedAxes = _.uniq( + data.filter(s => meta.enabled[seriesToKey(s)] !== newEnabled[seriesToKey(s)]) + .map(s => s.Unit) + .filter(unit => unit != null) + ); + const newLimits = { ...meta.yLimits }; + const primaryAxis = getPrimaryAxis(key); + const [start, end] = meta.key.DataType === 'FFT' + ? prev.fftLimits + : meta.key.DataType === 'OverlappingWave' + ? prev.cycleLimits + : [prev.startTime, prev.endTime]; + + affectedAxes.forEach(axis => { + const relevantData = data.filter(item => newEnabled[seriesToKey(item)] && item.Unit === axis); + const recomputed = recomputeDataLimits(start, end, relevantData, meta.yLimits[axis].current); + newLimits[axis] = { ...newLimits[axis], dataLimits: recomputed }; + newLimits[axis] = { + ...newLimits[axis], + zoomedLimits: recomputeNonAutoLimits( + meta.yLimits[primaryAxis].dataLimits, + meta.yLimits[primaryAxis].zoomedLimits, + recomputed + ) + }; + updateActiveUnits(newLimits, axis, relevantData, prev.startTime, prev.endTime, null); + }); + + return { + ...prev, + meta: { + ...prev.meta, + [pk]: { + ...meta, + enabled: newEnabled, + selectedAssets: [...selectedAssets], + selectedTraces: [...selectedTraces], + legendSelectionsUserSet: true, + yLimits: newLimits + } + } + }; + }); + }, + + SetIsManual: (key, unit, manual) => { + const pk = toPlotKey(key); + setState(prev => { + const meta = prev.meta[pk]; + if (!meta) return prev; + + const newLimits = { ...meta.yLimits }; + newLimits[unit] = { ...newLimits[unit], isManual: manual }; + + const isValidNumber = (v: number) => !isNaN(v) && isFinite(v); + const invalidZoomed = !newLimits[unit].zoomedLimits || + !isValidNumber(newLimits[unit].zoomedLimits[0]) || + !isValidNumber(newLimits[unit].zoomedLimits[1]); + const invalidData = !newLimits[unit].dataLimits || + !isValidNumber(newLimits[unit].dataLimits[0]) || + !isValidNumber(newLimits[unit].dataLimits[1]); + + if (meta.isZoomed && !invalidZoomed) + newLimits[unit] = { ...newLimits[unit], manualLimits: newLimits[unit].zoomedLimits }; + else if (!invalidData) + newLimits[unit] = { ...newLimits[unit], manualLimits: newLimits[unit].dataLimits }; + + return { + ...prev, + meta: { + ...prev.meta, + [pk]: { ...meta, yLimits: newLimits } + } + }; + }); + }, + + SetManualLimits: (limits, key, axis, auto, data, factor) => { + const pk = toPlotKey(key); + setState(prev => { + const meta = prev.meta[pk]; + if (!meta) return prev; + + const newLimits = { ...meta.yLimits }; + newLimits[axis] = { ...newLimits[axis], isManual: true, manualLimits: limits }; + + if (meta.isZoomed) + newLimits[axis] = { ...newLimits[axis], zoomedLimits: limits }; + + if (auto) { + const relevantData = data.filter(item => meta.enabled[seriesToKey(item)] && item.Unit === axis); + const idx = updateActiveUnits(newLimits, axis, relevantData, prev.startTime, prev.endTime, limits); + if (idx >= 0) { + const f = factor ?? 1; + newLimits[axis] = { + ...newLimits[axis], + current: idx, + manualLimits: [limits[0] * f, limits[1] * f] + }; + } + } + + return { + ...prev, + meta: { + ...prev.meta, + [pk]: { ...meta, yLimits: newLimits } + } + }; + }); + }, + + SetSelectPoint: (time, plotData) => { + setState(prev => { + const newMeta = { ...prev.meta }; + Object.keys(newMeta).forEach(pk => { + const m = newMeta[pk]; + const d = plotData[pk] ?? []; + if (d.length === 0) return; + + const shortest = _.minBy(d, s => s.DataPoints.length); + const idx = getIndex(time, shortest?.DataPoints ?? []); + if (isNaN(idx)) return; + + newMeta[pk] = { + ...m, + selectedIndices: [...m.selectedIndices, idx], + selectedTimes: [...(m.selectedTimes ?? []), time] + }; + }); + return { ...prev, meta: newMeta }; + }); + }, + + ClearSelectPoints: () => { + setState(prev => { + const newMeta = { ...prev.meta }; + Object.keys(newMeta).forEach(pk => { + newMeta[pk] = { ...newMeta[pk], selectedIndices: [], selectedTimes: [] }; + }); + return { ...prev, meta: newMeta }; + }); + }, + + RemoveSelectPoints: (index) => { + setState(prev => { + const newMeta = { ...prev.meta }; + Object.keys(newMeta).forEach(pk => { + const indices = [...newMeta[pk].selectedIndices]; + const times = [...(newMeta[pk].selectedTimes ?? [])]; + indices.splice(index, 1); + times.splice(index, 1); + newMeta[pk] = { ...newMeta[pk], selectedIndices: indices, selectedTimes: times }; + }); + return { ...prev, meta: newMeta }; + }); + }, + + InitPlotMeta: (key, yLimits, isZoomed) => { + const pk = toPlotKey(key); + setState(prev => { + const existing = prev.meta[pk]; + const meta = existing ?? createEmptyMeta(key); + + const localUnits = getLocalUnitSettings(key.DataType); + const newLimits = { ...meta.yLimits }; + if (localUnits) { + Object.keys(localUnits).forEach(u => { + if (newLimits[u]) { + newLimits[u] = { + ...newLimits[u], + current: localUnits[u].current, + isAuto: localUnits[u].isAuto + }; + } + }); + } + + if (yLimits) { + Object.keys(yLimits).forEach(u => { + newLimits[u] = yLimits[u]; + }); + } + + return { + ...prev, + meta: { + ...prev.meta, + [pk]: { + ...meta, + key, + loading: 'Loading', + yLimits: newLimits, + isZoomed: isZoomed ?? meta.isZoomed + } + } + }; + }); + }, + + RemovePlotMeta: (key) => { + const pk = toPlotKey(key); + setState(prev => { + const newMeta = { ...prev.meta }; + delete newMeta[pk]; + return { ...prev, meta: newMeta }; + }); + }, + + SetPlotLoading: (key, loading) => { + const pk = toPlotKey(key); + setState(prev => { + const meta = prev.meta[pk]; + if (!meta) return prev; + return { + ...prev, + meta: { ...prev.meta, [pk]: { ...meta, loading } } + }; + }); + }, + + OnDataAppended: (key, data, defaultEnabled) => { + const pk = toPlotKey(key); + setState(prev => { + const meta = prev.meta[pk]; + if (!meta || data.length === 0) return prev; + + const selections = meta.legendSelectionsUserSet ? + { selectedAssets: meta.selectedAssets, selectedTraces: meta.selectedTraces } : + getLegendSelectionsFromEnabled(data, defaultEnabled); + const newEnabled = getEnabledFromLegendSelections(data, selections.selectedAssets, selections.selectedTraces); + const isEnabled = (s: OpenSee.iD3DataSeries) => newEnabled[seriesToKey(s)] === true; + + // Correct time range FIRST if data doesn't overlap + let newStart = prev.startTime; + let newEnd = prev.endTime; + let newFft = prev.fftLimits; + let newCycleLimits = prev.cycleLimits; + + if (key.DataType === 'FFT') { + const enabledData = data.filter(isEnabled); + if (enabledData.length > 0) { + newFft = [ + Math.min(...enabledData.map(s => Math.min(...s.DataPoints.map(p => p[0])))), + Math.max(...enabledData.map(s => Math.max(...s.DataPoints.map(p => p[0])))) + ]; + } + } else if (key.DataType !== 'OverlappingWave') { + const enabledData = data.filter(s => isEnabled(s) && s.DataPoints.length > 0); + if (enabledData.length > 0) { + const dataMin = Math.min(...enabledData.map(s => s.DataPoints[0][0])); + const dataMax = Math.max(...enabledData.map(s => s.DataPoints[s.DataPoints.length - 1][0])); + if (prev.endTime < dataMin || prev.startTime > dataMax) { + newStart = dataMin; + newEnd = dataMax; + } + } + } else { + const enabledData = data.filter(s => isEnabled(s) && s.DataPoints.length > 0); + const xValues = enabledData.flatMap(s => s.DataPoints.map(p => p[0]).filter(Number.isFinite)); + if (xValues.length > 0) + newCycleLimits = [Math.min(...xValues), Math.max(...xValues)]; + } + + // NOW compute limits with the corrected time range + const newLimits = { ...meta.yLimits }; + const [s, e] = key.DataType === 'FFT' + ? newFft + : key.DataType === 'OverlappingWave' + ? newCycleLimits + : [newStart, newEnd]; + + const axes = _.uniq(data.map(s => s.Unit)); + axes.forEach(axis => { + const filtered = data.filter(s => isEnabled(s) && s.Unit === axis); + const autoIdx = updateActiveUnits(newLimits, axis, filtered, s, e, null); + if (autoIdx >= 0) + newLimits[axis] = { ...newLimits[axis], current: autoIdx }; + }); + + const updatedMeta: IPlotMeta = { + ...meta, + enabled: newEnabled, + selectedAssets: selections.selectedAssets, + selectedTraces: selections.selectedTraces, + yLimits: updateAutoLimits({ ...meta, enabled: newEnabled, yLimits: newLimits }, data, s, e) + }; + + return { + ...prev, + startTime: newStart, + endTime: newEnd, + fftLimits: newFft, + cycleLimits: newCycleLimits, + meta: { ...prev.meta, [pk]: updatedMeta } + }; + }); + }, + }), []); + + return ( + + + {props.children} + + + ); +}; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotStateUtilities.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotStateUtilities.tsx new file mode 100644 index 00000000..629250ca --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Context/PlotStateUtilities.tsx @@ -0,0 +1,545 @@ +// PlotUtilities.ts +// Pure utility functions for computing axis limits, defaults, display names, etc. +// These operate on data arrays and meta objects independently rather than +// the old monolithic IGraphstate. + +import _ from 'lodash'; +import { OpenSee } from '../global'; +import { defaultSettings } from '../defaults'; +import { PlotKey, SeriesKey, LegendTraceKey, seriesToKey, seriesToLegendTraceKey } from './PlotKeys'; + +// Lightweight per-plot metadata that lives in PlotStateContext. +// The heavy iD3DataSeries arrays live separately in PlotDataContext. +export interface IPlotMeta { + key: OpenSee.IGraphProps; + loading: OpenSee.LoadingState; + enabled: Record; + selectedAssets: string[]; + selectedTraces: LegendTraceKey[]; + legendSelectionsUserSet: boolean; + selectedIndices: number[]; + selectedTimes: number[]; + isZoomed: boolean; + yLimits: OpenSee.IUnitCollection; +} + +export const defaultAxisSettings: OpenSee.IAxisSettings = { + isManual: false, + dataLimits: [0, 1], + manualLimits: [0, 1], + zoomedLimits: [0, 1], + isAuto: false, + current: 0 +}; + +function createDefaultAxisSettings(unit: keyof OpenSee.IUnitCollection): OpenSee.IAxisSettings { + const setting = defaultSettings.Units[unit]; + return { ...defaultAxisSettings, current: setting.current, isAuto: setting.autoUnit }; +} + +export function createDefaultYLimits(): OpenSee.IUnitCollection { + return { + Voltage: createDefaultAxisSettings('Voltage'), + Current: createDefaultAxisSettings('Current'), + Angle: createDefaultAxisSettings('Angle'), + VoltageperSecond: createDefaultAxisSettings('VoltageperSecond'), + CurrentperSecond: createDefaultAxisSettings('CurrentperSecond'), + Freq: createDefaultAxisSettings('Freq'), + Impedance: createDefaultAxisSettings('Impedance'), + PowerP: createDefaultAxisSettings('PowerP'), + PowerQ: createDefaultAxisSettings('PowerQ'), + PowerS: createDefaultAxisSettings('PowerS'), + PowerPf: createDefaultAxisSettings('PowerPf'), + TCE: createDefaultAxisSettings('TCE'), + Distance: createDefaultAxisSettings('Distance'), + Unbalance: createDefaultAxisSettings('Unbalance'), + THD: createDefaultAxisSettings('THD'), + [""]: createDefaultAxisSettings('') + }; +} + +export function createEmptyMeta(key: OpenSee.IGraphProps): IPlotMeta { + return { + key, + loading: 'Idle', + enabled: {}, + selectedAssets: [], + selectedTraces: [], + legendSelectionsUserSet: false, + selectedIndices: [], + selectedTimes: [], + isZoomed: false, + yLimits: createDefaultYLimits() + }; +} + +export function getLegendSelectionsFromEnabled( + data: OpenSee.iD3DataSeries[], + enabled: Record +): { selectedAssets: string[], selectedTraces: LegendTraceKey[] } { + const selectedAssets: string[] = []; + const selectedTraces: LegendTraceKey[] = []; + + data.forEach(item => { + if (enabled[seriesToKey(item)] !== true) return; + + const traceKey = seriesToLegendTraceKey(item); + if (!selectedAssets.includes(item.LegendGroup)) selectedAssets.push(item.LegendGroup); + if (!selectedTraces.includes(traceKey)) selectedTraces.push(traceKey); + }); + + return { selectedAssets, selectedTraces }; +} + +export function getEnabledFromLegendSelections( + data: OpenSee.iD3DataSeries[], + selectedAssets: string[], + selectedTraces: LegendTraceKey[] +): Record { + const enabled: Record = {}; + + data.forEach(item => { + enabled[seriesToKey(item)] = + selectedAssets.includes(item.LegendGroup) && + selectedTraces.includes(seriesToLegendTraceKey(item)); + }); + + return enabled; +} + +// Binary-ish search for the index closest to time t in a sorted array. +// Assumes uniform spacing between data points. +export function getIndex(t: number, data: Array<[number, number]>): number { + if (data == null || data.length < 2) + return NaN; + + if (t < data[0][0]) + return 0; + if (t > data[data.length - 1][0]) + return data.length - 1; + + const dP = data[1][0] - data[0][0]; + const deltaT = t - data[0][0]; + return Math.floor(deltaT / dP); +} + +// Compute y-axis [min, max] for a set of enabled series within a given x range. +// Applies the active unit factor to scale values appropriately. +export function recomputeDataLimits( + start: number, + end: number, + data: OpenSee.iD3DataSeries[], + activeUnit: number +): [number, number] { + if (data.length === 0) + return [0, 1]; + + let yMin = Number.POSITIVE_INFINITY; + let yMax = Number.NEGATIVE_INFINITY; + + data.forEach(item => { + let dataPoints = item.DataPoints; + if (item.SmoothDataPoints.length > 0) + dataPoints = item.SmoothDataPoints; + + const indexStart = getIndex(start, dataPoints); + const indexEnd = getIndex(end, dataPoints); + + if (isNaN(indexStart) || isNaN(indexEnd)) + return; + + let factor = 1; + const unit: OpenSee.IUnitSetting | undefined = defaultSettings.Units[item.Unit]; + if (unit != null) + factor = unit?.options?.[activeUnit]?.factor ?? 1; + + // per-unit case + if (factor === undefined) + factor = 1.0 / item.BaseValue; + + const startIndex = Math.max(0, Math.min(indexStart, dataPoints.length)); + const endIndex = Math.max(startIndex, Math.min(indexEnd, dataPoints.length)); + let itemMin = Number.POSITIVE_INFINITY; + let itemMax = Number.NEGATIVE_INFINITY; + + for (let i = startIndex; i < endIndex; i++) { + const value = dataPoints[i][1]; + if (isNaN(value) || !isFinite(value)) + continue; + + if (value < itemMin) + itemMin = value; + if (value > itemMax) + itemMax = value; + } + + if (!isFinite(itemMin) || !isFinite(itemMax)) + return; + + yMin = Math.min(yMin, itemMin * factor); + yMax = Math.max(yMax, itemMax * factor); + }); + + if (!isFinite(yMin) || !isFinite(yMax)) + return [0, 1]; + + if (yMin === yMax) { + if (data.some(item => item.Unit === "" && (yMin === 0 || yMin === 1))) + return [-0.05, 1.05]; + + const flatPad = Math.max(Math.abs(yMin) / 20, 1); + return [yMin - flatPad, yMax + flatPad]; + } + + const pad = (yMax - yMin) / 20; + return [yMin - pad, yMax + pad]; +} + +// Recompute auto limits for all relevant axes on a single plot. +// Reads from the data array but only writes to the meta's yLimits. +export function updateAutoLimits( + meta: IPlotMeta, + data: OpenSee.iD3DataSeries[], + startTime: number, + endTime: number +): OpenSee.IUnitCollection { + if (data.length === 0) + return meta.yLimits; + + const newLimits = { ...meta.yLimits }; + const relevantAxes = _.uniq(data.map(s => s.Unit)); + + relevantAxes.forEach(axis => { + const isAuto = !meta.isZoomed && !meta.yLimits[axis].isManual; + if (!isAuto) return; + + const enabled = data.filter((item) => + item.Unit === axis && meta.enabled[seriesToKey(item)] !== false + ); + const computed = recomputeDataLimits(startTime, endTime, enabled, meta.yLimits[axis].current); + if (computed) + newLimits[axis] = { ...newLimits[axis], dataLimits: computed }; + }); + + return newLimits; +} + +// Proportionally rescale one axis's limits based on how another axis was zoomed +export function recomputeNonAutoLimits( + oldLimits: [number, number], + newLimits: [number, number], + currentLimits: [number, number] +): [number, number] { + const oldRange = oldLimits[1] - oldLimits[0]; + const lowerProportion = (newLimits[0] - oldLimits[0]) / oldRange; + const upperProportion = (newLimits[1] - oldLimits[0]) / oldRange; + + const currentRange = currentLimits[1] - currentLimits[0]; + return [ + currentLimits[0] + lowerProportion * currentRange, + currentLimits[0] + upperProportion * currentRange + ]; +} + +// Scale limits when switching between unit options that have fixed factors +export function scaleLimitsByFactor( + oldIndex: number, + newIndex: number, + unit: OpenSee.Unit, + limits: [number, number] +): [number, number] { + const oldFactor = defaultSettings.Units[unit].options[oldIndex].factor; + const newFactor = defaultSettings.Units[unit].options[newIndex].factor; + const change = newFactor / oldFactor; + return [limits[0] * change, limits[1] * change]; +} + +// Scale limits proportionally when the data range itself changes (e.g. per-unit toggle) +export function scaleLimits( + oldDataLimits: [number, number], + newDataLimits: [number, number], + zoomedLimits: [number, number] +): [number, number] { + const oldRange = oldDataLimits[1] - oldDataLimits[0]; + const newRange = newDataLimits[1] - newDataLimits[0]; + const scale = newRange / oldRange; + return [zoomedLimits[0] * scale, zoomedLimits[1] * scale]; +} + +// Determines which auto unit factor to use based on the magnitude of the data +export function updateActiveUnits( + units: OpenSee.IUnitCollection, + unit: OpenSee.Unit, + data: OpenSee.iD3DataSeries[], + startTime: number, + endTime: number, + manualLimits: [number, number] | null +): number { + if (!units[unit].isAuto) + return -1; + + let min = Number.POSITIVE_INFINITY; + let max = Number.NEGATIVE_INFINITY; + + data.filter(d => d.Unit === unit).forEach(d => { + const startIndex = getIndex(startTime, d.DataPoints); + const endIndex = getIndex(endTime, d.DataPoints); + + if (isNaN(startIndex) || isNaN(endIndex)) + return; + + const firstIndex = Math.max(0, Math.min(startIndex, d.DataPoints.length)); + const lastIndex = Math.max(firstIndex, Math.min(endIndex, d.DataPoints.length)); + + for (let i = firstIndex; i < lastIndex; i++) { + const value = d.DataPoints[i][1]; + if (isNaN(value) || !isFinite(value)) + continue; + + if (value < min) + min = value; + if (value > max) + max = value; + } + }); + + if (manualLimits) { + min = manualLimits[0]; + max = manualLimits[1]; + } + + if (!isFinite(min) || !isFinite(max)) + return -1; + + const magnitude = Math.max(Math.abs(min), Math.abs(max)); + + let autoFactor = 0.000001; + if (magnitude < 1) + autoFactor = 1000; + else if (magnitude < 1000) + autoFactor = 1; + else if (magnitude < 1000000) + autoFactor = 0.001; + + const options = defaultSettings.Units[unit].options; + let idx = options.findIndex(item => item.factor === autoFactor); + if (idx >= 0) return idx; + + // fallback: try adjacent factor + autoFactor = autoFactor < 1 ? autoFactor * 1000 : 1; + idx = options.findIndex(item => item.factor === autoFactor); + if (idx >= 0) return idx; + + return options.findIndex(item => item.factor !== 0); +} + +// Map from graphType to its primary y-axis unit +export function getPrimaryAxis(key: OpenSee.IGraphProps): OpenSee.Unit { + switch (key.DataType) { + case 'Voltage': return 'Voltage'; + case 'Current': return 'Current'; + case 'Analogs': + case 'Digitals': + return ''; + case 'FirstDerivative': return 'VoltageperSecond'; + case 'Unbalance': return 'Unbalance'; + case 'THD': return 'THD'; + case 'RemoveCurrent': return 'Current'; + case 'Power': return 'PowerP'; + case 'Impedance': return 'Impedance'; + case 'Frequency': return 'Freq'; + case 'FaultDistance': return 'Distance'; + case 'I2T': return 'Current'; + default: return 'Voltage'; + } +} + +// Build a display name for tooltip entries +export function getDisplayName(d: OpenSee.iD3DataSeries, type: OpenSee.graphType, harmonic?: number): string { + const harmonicLabel = harmonic == null ? '' : ` ${harmonic}`; + + switch (type) { + case 'Voltage': + case 'Current': + return d.LegendGroup + (type === 'Voltage' ? ' V ' : ' I ') + d.LegendVertical + ' ' + d.LegendHorizontal; + case 'FirstDerivative': + return `${d.LegendGroup} Derivative`; + case 'ClippedWaveforms': + return `${d.LegendGroup} Fixed Clipped Waveform`; + case 'Frequency': + return `${d.LegendGroup} Frequency${d.LegendVertical === 'Avg' ? ' Avg' : ''}`; + case 'HighPassFilter': + return `${d.LegendGroup} HPF`; + case 'LowPassFilter': + return `${d.LegendGroup} LPF`; + case 'FaultDistance': + return d.LegendVertical; + case 'FFT': + return `FFT ${d.LegendGroup} ${d.LegendHorizontal}`; + case 'Impedance': + case 'Power': + case 'SymetricComp': + return `${d.LegendHorizontal} ${d.LegendVertical}`; + case 'MissingVoltage': + return `${d.LegendGroup} Missing Voltage (${d.LegendHorizontal}-Fault)`; + case 'OverlappingWave': + return `${d.LegendGroup} Overlapping Waveform`; + case 'RapidVoltage': + return `${d.LegendGroup} Rapid Voltage Change`; + case 'Rectifier': + return `Rectifier ${d.LegendHorizontal === 'I' ? 'Current' : 'Voltage'}`; + case 'RemoveCurrent': + return `${d.LegendGroup} Removed Current (${d.LegendHorizontal}-Fault)`; + case 'Harmonic': + return `${d.LegendGroup} Harmonic${harmonicLabel} ${d.LegendHorizontal}`; + case 'THD': + return `${d.LegendGroup} THD`; + case 'Unbalance': + return `${d.LegendHorizontal}${d.LegendVertical} Unbalance`; + case 'I2T': + return `${d.LegendGroup} I2T`; + default: + return type; + } +} + +// Compute default enabled flags for each series based on graph type and user prefs +export function getDefaultEnabled( + type: OpenSee.graphType, + defaultTraces: OpenSee.IDefaultTrace, + defaultVoltage: 'L-L' | 'L-N', + data: OpenSee.iD3DataSeries[] +): Record { + // Helper: applies a predicate per series and builds a keyed record + function buildMap(predicate: (item: OpenSee.iD3DataSeries) => boolean): Record { + const result: Record = {}; + data.forEach(item => { result[seriesToKey(item)] = predicate(item); }); + return result; + } + + switch (type) { + case 'Voltage': + return buildMap(item => + item.LegendVGroup === defaultVoltage && + ((item.LegendHorizontal === 'Ph' && defaultTraces.Ph) || + (item.LegendHorizontal === 'RMS' && defaultTraces.RMS) || + (item.LegendHorizontal === 'Pk' && defaultTraces.Pk) || + (item.LegendHorizontal === 'W' && defaultTraces.W)) + ); + case 'Current': + return buildMap(item => + (item.LegendHorizontal === 'Ph' && defaultTraces.Ph) || + (item.LegendHorizontal === 'RMS' && defaultTraces.RMS) || + (item.LegendHorizontal === 'Pk' && defaultTraces.Pk) || + (item.LegendHorizontal === 'W' && defaultTraces.W) + ); + case 'FaultDistance': + return buildMap(item => + item.LegendVertical === 'Simple' || + item.LegendVertical === 'Reactance' || + item.LegendVertical === 'Takagi' || + item.LegendVertical === 'ModifiedTakagi' || + item.LegendVertical === 'Novosel' + ); + case 'FirstDerivative': + return buildMap(item => + ((item.LegendHorizontal === 'W' && defaultTraces.W) || + (item.LegendHorizontal === 'RMS' && defaultTraces.RMS)) && + item.LegendVertical !== 'NG' && item.LegendVertical !== 'RES' + ); + case 'ClippedWaveforms': + case 'Frequency': + case 'HighPassFilter': + case 'LowPassFilter': + case 'MissingVoltage': + case 'OverlappingWave': + case 'RapidVoltage': + case 'THD': + case 'I2T': + return buildMap(item => + item.LegendVertical === 'AN' || item.LegendVertical === 'BN' || item.LegendVertical === 'CN' + ); + case 'Power': + return buildMap(item => + (item.LegendVertical === 'AN' || item.LegendVertical === 'BN' || item.LegendVertical === 'CN') && + item.LegendHorizontal === 'P' + ); + case 'Impedance': + return buildMap(item => + (item.LegendVertical === 'AN' || item.LegendVertical === 'BN' || item.LegendVertical === 'CN') && + item.LegendHorizontal === 'R' + ); + case 'Rectifier': + return buildMap(item => item.LegendHorizontal === 'V'); + case 'SymetricComp': + return buildMap(item => item.LegendVertical === 'Pos'); + case 'Unbalance': + return buildMap(item => item.LegendVertical === 'Neg/Pos'); + case 'FFT': + return buildMap(item => item.LegendHorizontal === 'Mag' && item.LegendVGroup === 'Volt.'); + case 'Harmonic': + return buildMap(item => item.LegendHorizontal === 'Mag'); + case 'RemoveCurrent': + return buildMap(item => item.LegendHorizontal === 'Pre'); + default: + return buildMap(() => false); + } +} + +// Read unit overrides from localStorage for a given plot type +export function getLocalUnitSettings(dataType: OpenSee.graphType): Record | null { + try { + const raw = localStorage.getItem('openSee.Settings'); + const settings = JSON.parse(raw ?? '{}'); + const unitSettings = settings.Units; + if (!Array.isArray(unitSettings)) return null; + + const match = unitSettings.find((s: any) => s.DataType === dataType); + return match?.Units ?? null; + } catch { + return null; + } +} + +// Persist current unit selections to localStorage +export function saveUnitSettings( + meta: Record, + data: Record +): void { + try { + const raw = localStorage.getItem('openSee.Settings'); + const settings = JSON.parse(raw ?? '{}'); + if (!Array.isArray(settings.Units)) + settings.Units = []; + + Object.keys(meta).forEach(plotKey => { + const m = meta[plotKey]; + const d = data[plotKey]; + if (!m || !d) return; + + const enabledUnits = _.uniq( + d.filter((series) => m.enabled[seriesToKey(series)]).map(s => s.Unit) + ); + + let entry = settings.Units.find((u: any) => u.DataType === m.key.DataType); + if (!entry) { + entry = { DataType: m.key.DataType, Units: {} }; + settings.Units.push(entry); + } + if (!entry.Units) entry.Units = {}; + + Object.keys(m.yLimits).forEach(unit => { + if (enabledUnits.includes(unit as OpenSee.Unit)) { + entry.Units[unit] = { + current: m.yLimits[unit].current, + isAuto: m.yLimits[unit].isAuto + }; + } + }); + }); + + localStorage.setItem('openSee.Settings', JSON.stringify(settings)); + } catch { + // ignore write errors + } +} diff --git a/src/OpenSEE/Scripts/TSX/store/GraphLogic.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Data/GraphLogic.tsx similarity index 86% rename from src/OpenSEE/Scripts/TSX/store/GraphLogic.tsx rename to src/OpenSEE/wwwroot/Scripts/TSX/Data/GraphLogic.tsx index 6deb0824..53dd4f78 100644 --- a/src/OpenSEE/Scripts/TSX/store/GraphLogic.tsx +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Data/GraphLogic.tsx @@ -21,10 +21,8 @@ // //****************************************************************************************************** -import _ from "lodash"; import { OpenSee } from "../global"; -import { ReplaceData, InitiateDetailed } from "./dataSlice" -import { useAppDispatch } from '../hooks'; +import $ from 'jquery'; const defaultLimits = { isManual: false, @@ -36,7 +34,10 @@ const defaultLimits = { } as OpenSee.IAxisSettings; export const emptygraph: OpenSee.IGraphstate = { - key: null, + key: { + EventId: -1, + DataType: 'Voltage', + }, data: [], loading: 'Idle', isZoomed: false, @@ -65,14 +66,14 @@ export const emptygraph: OpenSee.IGraphstate = { // #region [ Async Functions ] //This Function Grabs the Data for this Graph - Note that cases with multiple Event ID's need to be treated seperatly at the end -export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStore, appendCallBack: (data: OpenSee.iD3DataSeries[], type: 'time'|'frequency') => void, detailedCallBack: (key: OpenSee.IGraphProps) => void): Array> { - let result = []; +export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticContext, appendCallBack: (data: OpenSee.iD3DataSeries[], type: 'time'|'frequency') => void, detailedCallBack: (key: OpenSee.IGraphProps) => void): Array> { + const result: Array> = []; switch (key.DataType) { case ('Current'): case ('Voltage'): - let handlePOW = $.ajax({ + const handlePOW = $.ajax({ type: "GET", url: `${homePath}api/OpenSEE/GetData?eventId=${key.EventId}` + `&type=${key.DataType}` + @@ -82,7 +83,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor cache: true, async: true }); - let handleFreq = $.ajax({ + const handleFreq = $.ajax({ type: "GET", url: `${homePath}api/OpenSEE/GetData?eventId=${key.EventId}` + `&type=${key.DataType}` + @@ -95,18 +96,17 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor handlePOW.then(data => { appendCallBack(data.Data, 'time') - detailedCallBack(key); }); handleFreq.then((data) => { appendCallBack(data.Data, 'frequency'); - detailedCallBack(key); }); + Promise.all([handlePOW, handleFreq]).then(() => detailedCallBack(key)); result.push(handlePOW); result.push(handleFreq); break; case ('Analogs'): - let breakerAnalogsDataHandle = $.ajax({ + const breakerAnalogsDataHandle = $.ajax({ type: "GET", url: `${homePath}api/OpenSEE/GetAnalogsData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -119,7 +119,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(breakerAnalogsDataHandle); break; case ('Digitals'): - let breakerDigitalsDataHandle = $.ajax({ + const breakerDigitalsDataHandle = $.ajax({ type: "GET", url: `${homePath}api/OpenSEE/GetBreakerData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -132,7 +132,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(breakerDigitalsDataHandle); break; case ('TripCoil'): - let waveformTCEDataHandle = $.ajax({ + const waveformTCEDataHandle = $.ajax({ type: "GET", url: `${homePath}api/OpenSEE/GetData?eventId=${key.EventId}` + `&type=TripCoilCurrent` + @@ -147,7 +147,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(waveformTCEDataHandle); break; case ('FirstDerivative'): - let derivativeDataHandle = $.ajax({ + const derivativeDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetFirstDerivativeData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -159,7 +159,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(derivativeDataHandle); break case ('ClippedWaveforms'): - let clippedWaveformDataHandle = $.ajax({ + const clippedWaveformDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetClippedWaveformsData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -171,7 +171,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(clippedWaveformDataHandle); break case ('Frequency'): - let freqencyAnalyticDataHandle = $.ajax({ + const freqencyAnalyticDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetFrequencyData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -183,7 +183,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(freqencyAnalyticDataHandle); break case ('HighPassFilter'): - let highPassFilterDataHandle = $.ajax({ + const highPassFilterDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetHighPassFilterData?eventId=${key.EventId}` + `&filter=${options.HPFOrder}`, @@ -196,7 +196,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(highPassFilterDataHandle); break case ('LowPassFilter'): - let lowPassFilterDataHandle = $.ajax({ + const lowPassFilterDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetLowPassFilterData?eventId=${key.EventId}` + `&filter=${options.LPFOrder}`, @@ -210,7 +210,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor break case ('Impedance'): - let impedanceDataHandle = $.ajax({ + const impedanceDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetImpedanceData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -223,7 +223,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(impedanceDataHandle); break case ('Power'): - let powerDataHandle = $.ajax({ + const powerDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetPowerData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -235,7 +235,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(powerDataHandle); break case ('MissingVoltage'): - let missingVoltageDataHandle = $.ajax({ + const missingVoltageDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetMissingVoltageData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -247,7 +247,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(missingVoltageDataHandle); break case ('OverlappingWave'): - let overlappingWaveformDataHandle = $.ajax({ + const overlappingWaveformDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetOverlappingWaveformData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -260,7 +260,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(overlappingWaveformDataHandle); break case ('Rectifier'): - let rectifierDataHandle = $.ajax({ + const rectifierDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetRectifierData?eventId=${key.EventId}` + `&Trc=${options.Trc}`, @@ -273,7 +273,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(rectifierDataHandle); break case ('RapidVoltage'): - let rapidVoltageChangeDataHandle = $.ajax({ + const rapidVoltageChangeDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetRapidVoltageChangeData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -285,7 +285,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(rapidVoltageChangeDataHandle); break case ('RemoveCurrent'): - let removeCurrentDataHandle = $.ajax({ + const removeCurrentDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetRemoveCurrentData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -297,7 +297,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(removeCurrentDataHandle); break case ('Harmonic'): - let specifiedHarmonicDataHandle = $.ajax({ + const specifiedHarmonicDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetSpecifiedHarmonicData?eventId=${key.EventId}` + `&specifiedHarmonic=${options.Harmonic}`, @@ -313,7 +313,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(specifiedHarmonicDataHandle); break case ('SymetricComp'): - let symmetricalComponentsDataHandle = $.ajax({ + const symmetricalComponentsDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetSymmetricalComponentsData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -325,7 +325,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(symmetricalComponentsDataHandle); break case ('THD'): - let thdDataHandle = $.ajax({ + const thdDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetTHDData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -340,7 +340,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(thdDataHandle); break case ('Unbalance'): - let unbalanceDataHandle = $.ajax({ + const unbalanceDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetUnbalanceData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -352,7 +352,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(unbalanceDataHandle); break case ('FaultDistance'): - let faultDistanceDataHandle = $.ajax({ + const faultDistanceDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetFaultDistanceData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -364,7 +364,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(faultDistanceDataHandle); break case ('Restrike'): - let breakerRestrikeDataHandle = $.ajax({ + const breakerRestrikeDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetBreakerRestrikeData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -376,7 +376,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(breakerRestrikeDataHandle); break case ('FFT'): - let fftAnalyticDataHandle = $.ajax({ + const fftAnalyticDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetFFTData?eventId=${key.EventId}&cycles=${options.FFTCycles}&startDate=${options.FFTStartTime}`, contentType: "application/json; charset=utf-8", @@ -391,7 +391,7 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(fftAnalyticDataHandle); break case ('I2T'): - let i2tDataHandle = $.ajax({ + const i2tDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetI2tData?eventId=${key.EventId}`, contentType: "application/json; charset=utf-8", @@ -406,21 +406,18 @@ export function getData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStor result.push(i2tDataHandle); break default: - return [] break; } - - return result; } -export function getDetailedData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticStore, callBack: (key: OpenSee.IGraphProps, data: OpenSee.iD3DataSeries[]) => void): Array> { - let result = []; +export function getDetailedData(key: OpenSee.IGraphProps, options: OpenSee.IAnalyticContext, callBack: (key: OpenSee.IGraphProps, data: OpenSee.iD3DataSeries[]) => void): Array> { + const result: Array> = []; switch (key.DataType) { case ('Current'): case ('Voltage'): - let handlePOW = $.ajax({ + const handlePOW = $.ajax({ type: "GET", url: `${homePath}api/OpenSEE/GetData?eventId=${key.EventId}&fullRes=1` + `&type=${key.DataType}` + @@ -431,7 +428,7 @@ export function getDetailedData(key: OpenSee.IGraphProps, options: OpenSee.IAnal cache: true, async: true }); - let handleFreq = $.ajax({ + const handleFreq = $.ajax({ type: "GET", url: `${homePath}api/OpenSEE/GetData?eventId=${key.EventId}&fullRes=1` + `&type=${key.DataType}` + @@ -451,7 +448,7 @@ export function getDetailedData(key: OpenSee.IGraphProps, options: OpenSee.IAnal break; case ('THD'): - let thdDataHandle = $.ajax({ + const thdDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetTHDData?eventId=${key.EventId}&fullRes=1`, contentType: "application/json; charset=utf-8", @@ -464,7 +461,7 @@ export function getDetailedData(key: OpenSee.IGraphProps, options: OpenSee.IAnal break; case ('Harmonic'): - let specifiedHarmonicDataHandle = $.ajax({ + const specifiedHarmonicDataHandle = $.ajax({ type: "GET", url: `${homePath}api/Analytic/GetSpecifiedHarmonicData?eventId=${key.EventId}&fullRes=1` + `&specifiedHarmonic=${options.Harmonic}`, @@ -483,4 +480,21 @@ export function getDetailedData(key: OpenSee.IGraphProps, options: OpenSee.IAnal return result; } +// This function is to get data for overlapping events +export function getOverlappingEvents(eventID: number, eventStartTime: string | null, eventEndTime: string | null): JQuery.jqXHR { + + const overlappingEventHandle = $.ajax({ + type: "GET", + url: `${homePath}api/OpenSEE/GetOverlappingEvents?eventId=${eventID}` + + `${eventStartTime != undefined ? `&startDate=${eventStartTime}` : ``}` + + `${eventEndTime != undefined ? `&endDate=${eventEndTime}` : ``}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }); + + return overlappingEventHandle; +} + // #endregion diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Data/RequestHandler.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Data/RequestHandler.tsx new file mode 100644 index 00000000..7fa78145 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Data/RequestHandler.tsx @@ -0,0 +1,119 @@ +//****************************************************************************************************** +// RequestHandler.tsx - Gbtc +// +// Copyright © 2021, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/05/2020 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +import { OpenSee } from "../global"; +const HandleStore = new Map[]>(); + +// Store only in-flight handles. Completed jqXHRs retain responseText/responseJSON, which can be very large for some events, so prune them as soon as they settle. +const TrackRequests = (target: string, requests: JQuery.jqXHR[]) => { + const pendingRequests = requests.filter(item => item != null && item.state() === 'pending'); + + pendingRequests.forEach(request => { + request.always(() => { + const targetValue = HandleStore.get(target); + if (targetValue == null) + return; + + const remaining = targetValue.filter(item => item !== request); + if (remaining.length > 0) + HandleStore.set(target, remaining); + else + HandleStore.delete(target); + }); + }); + + if (pendingRequests.length > 0) + HandleStore.set(target, pendingRequests); + else + HandleStore.delete(target); +} + +//Functions to Handle Requests. +const AddRequest = (key: OpenSee.IGraphProps, requests: JQuery.jqXHR[]) => { + const target = key.DataType.toString() + '-' + key.EventId.toString(); + + const targetValue = HandleStore.get(target); + + if (targetValue != null) + targetValue.forEach(item => { if (item != null && item.abort != null) item.abort(); }) + + TrackRequests(target, requests); +} + +const CancelAnalytics = () => { + for (const key of HandleStore.keys()) { + if (key.startsWith('Voltage-') || key.startsWith('Current-') || key.startsWith("Analogs-") || key.startsWith("Digitals-") || key.startsWith('TripCoil-')) + continue; + + const targetValue = HandleStore.get(key); + if (targetValue != null) + targetValue.forEach(item => { if (item != null && item.abort != null) item.abort(); }) + + HandleStore.delete(key); + } +} + +const CancelCompare = (baseEventID: number) => { + for (const key of HandleStore.keys()) { + if (key.endsWith('-' + baseEventID.toString())) + continue; + + const targetValue = HandleStore.get(key); + if (targetValue != null) + targetValue.forEach(item => { if (item != null && item.abort != null) item.abort(); }) + + HandleStore.delete(key); + } +} + +const CancelEvent = (eventId: number) => { + for (const key of HandleStore.keys()) { + if (!key.endsWith('-' + eventId.toString())) + continue; + + const targetValue = HandleStore.get(key); + if (targetValue != null) + targetValue.forEach(item => { if (item != null && item.abort != null) item.abort(); }) + + HandleStore.delete(key); + } +} + +const AppendRequest = (key: OpenSee.IGraphProps, requests: JQuery.jqXHR[]) => { + const target = key.DataType.toString() + '-' + key.EventId.toString(); + let r = requests; + + const targetValue = HandleStore.get(target); + if (targetValue != null) + r = [...r, ...targetValue]; + + TrackRequests(target, r) +} + +export { + CancelAnalytics, + AddRequest, + CancelEvent, + CancelCompare, + AppendRequest +} \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart.tsx new file mode 100644 index 00000000..f437c312 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart.tsx @@ -0,0 +1,249 @@ +//****************************************************************************************************** +// BarChartBase.tsx - 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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. +// +//****************************************************************************************************** + +import * as d3 from "d3"; +import * as React from 'react'; +import { AnalyticContext, SelectAnalyticOptions } from '../Context/AnalyticContext'; +import { PlotDataStateContext } from '../Context/PlotDataContext'; +import { PlotStateStateContext } from '../Context/PlotStateContext'; +import { toPlotKey } from '../Context/PlotKeys'; +import { selectYLimits, selectYLabels, selectActiveUnit, selectRelevantUnits, selectEnabledUnits } from '../PlotSelectors'; +import { getPrimaryAxis } from '../Context/PlotStateUtilities'; +import { OpenSee } from '../global'; +import { useAppSelector } from '../hooks'; +import { SelectColor, SelectMouseMode, SelectZoomMode } from '../Store/settingSlice'; +import Legend from './Legend/Legend'; +import ChartContainer from './ChartContainer'; +import { computeZoomWindow } from './Renderers/ZoomWindow'; +import { updateYAxes, updateYAxisLabels, updateYAxisPositionsOnResize, updateYAxisVisibility } from './Renderers/YAxes'; +import { drawBars, updateBarColors, updateBarGeometry, updateBarVisibility } from './BarChart/Renderers/Bars'; +import { drawAnglePoints, updateAnglePointColors, updateAnglePointGeometry, updateAnglePointVisibility } from './BarChart/Renderers/AnglePoints'; +import { CreateBarPlotFrame } from './BarChart/Renderers/CreateBarPlotFrame'; +import { setFrequencyDomainFromBands, syncFrequencyScaleRange, updateFrequencyXAxisOnResize, updateFrequencyXAxisTicks } from './BarChart/Renderers/XAxisFreq'; +import { useChartScales, useYLabelFontSize } from "./Utils/Utilities"; +import { IBarScales, useBarMouseInteractions } from "./BarChart/Utils"; + +interface IProps { + height: number, + width: number, + dataKey: OpenSee.IGraphProps +} + +const BarChart = (props: IProps) => { + const [analytic] = React.useContext(AnalyticContext); + const { plots: plotData } = React.useContext(PlotDataStateContext); + const plotState = React.useContext(PlotStateStateContext); + + const dataKey: OpenSee.IGraphProps = { DataType: props.dataKey.DataType, EventId: props.dataKey.EventId }; + const pk = toPlotKey(dataKey); + const plotMeta = plotState.meta[pk]; + const barData = plotData[pk] ?? []; + const enabledBar = plotMeta?.enabled ?? {}; + const loading = plotMeta?.loading ?? 'Uninitiated'; + + const primaryAxis = getPrimaryAxis(dataKey); + const activeUnit = React.useMemo(() => plotMeta ? selectActiveUnit(plotMeta) : null, [plotMeta]); + const relevantUnits = React.useMemo(() => selectRelevantUnits(dataKey, barData), [barData]); + const enabledUnits = React.useMemo(() => selectEnabledUnits(dataKey, barData, plotMeta?.enabled ?? {}), [barData, plotMeta?.enabled]); + const yLimits = React.useMemo(() => plotMeta ? selectYLimits(plotMeta) : {}, [plotMeta]); //type properly + const yLabels = React.useMemo(() => plotMeta ? selectYLabels(plotMeta) : {}, [plotMeta]); //type properly + const options = React.useMemo(() => SelectAnalyticOptions(analytic, props.dataKey.DataType), [analytic, props.dataKey]); + + const colors = useAppSelector(SelectColor); + const mouseMode = useAppSelector(SelectMouseMode); + const zoomMode = useAppSelector(SelectZoomMode); + + const containerRef = React.useRef(null); + const { xScaleRef, yScaleRef } = useChartScales(d3.scaleBand([], [0, 0])); + const xScaleLblRef = React.useRef>(d3.scaleLinear().domain([0, 1]).range([0, 0])); + const [hover, setHover] = React.useState<[number, number]>([0, 0]); + const [isCreated, setCreated] = React.useState(false); + const yLblFontSize = useYLabelFontSize(yLabels, primaryAxis, props.height); + + const { handlers, mouseDown, pointMouse } = useBarMouseInteractions({ + containerRef, + xScaleRef, + yScaleRef, + primaryAxis, + hover, + setHover, + mouseMode, + zoomMode, + plotData, + dataKey, + height: props.height, + width: props.width, + fftLimits: plotState.fftLimits, + yLimits + }); + + const getScales = (): IBarScales => ({ + x: xScaleRef.current, + y: yScaleRef.current as Record> + }); + + const syncDomains = () => { + relevantUnits.forEach(unit => { + if ((yScaleRef.current as any)[unit] && yLimits?.[unit]) + (yScaleRef.current as any)[unit].domain(yLimits[unit]); + }); + + const domain = (barData?.[0]?.DataPoints ?? []) + .filter(pt => pt[0] >= plotState.fftLimits[0] && pt[0] <= plotState.fftLimits[1]) + .map(pt => pt[0]); + + xScaleRef.current.domain(domain).range([60, props.width - 110]); + setFrequencyDomainFromBands(xScaleRef.current, xScaleLblRef.current); + syncFrequencyScaleRange(xScaleRef.current, xScaleLblRef.current, props.width); + } + + const rebuildPlot = () => { + CreateBarPlotFrame( + containerRef.current, + xScaleRef, + xScaleLblRef, + yScaleRef as { current: Record> }, + { + height: props.height, + width: props.width, + dataKey, + yLimits, + enabledUnits, + yLabels, + barData, + fftLimits: plotState.fftLimits + }, + handlers + ); + drawBars(containerRef.current, barData, getScales(), colors, enabledBar, activeUnit, props.height - 40); + drawAnglePoints(containerRef.current, barData, getScales(), colors, enabledBar, activeUnit); + updateLimits(); + updateVisibility(); + } + + const updateLimits = () => { + const scales = getScales(); + updateBarGeometry(containerRef.current, scales, activeUnit, props.height - 40); + updateAnglePointGeometry(containerRef.current, scales, activeUnit); + updateYAxes(containerRef.current, enabledUnits, scales.y, props.width, dataKey.DataType, dataKey.EventId); + updateFrequencyXAxisTicks(containerRef.current, xScaleLblRef.current); + } + + const updateVisibility = () => { + updateBarVisibility(containerRef.current, barData, enabledBar); + updateAnglePointVisibility(containerRef.current, barData, enabledBar); + updateYAxisVisibility(containerRef.current, relevantUnits, enabledUnits); + } + + React.useEffect(() => { + if (barData == null || barData.length === 0 || loading === 'Loading') return; + rebuildPlot(); + setCreated(true); + }, [barData, loading]); + + React.useEffect(() => { + if (!isCreated || xScaleRef.current == null || yScaleRef.current == null) return; + + xScaleRef.current.range([60, props.width - 110]); + updateFrequencyXAxisOnResize(containerRef.current, xScaleRef.current, xScaleLblRef.current, props.height, props.width); + updateYAxisPositionsOnResize(containerRef.current, relevantUnits, getScales().y, props.height, props.width); + + relevantUnits.forEach(unit => { + if ((yScaleRef.current as any)[unit] != null) + (yScaleRef.current as any)[unit].range([props.height - 40, 20]); + }); + + d3.select(containerRef.current).select(".clip").attr("height", props.height - 60).attr("width", props.width - 170); + d3.select(containerRef.current).select(".Overlay").attr("width", props.width - 110); + updateLimits(); + }, [props.height, props.width]); + + React.useEffect(() => { + if (barData == null || barData.length === 0) return; + updateVisibility(); + }, [enabledBar]); + + React.useEffect(() => { + if (!isCreated || yScaleRef.current == null || xScaleRef.current == null) return; + syncDomains(); + if (yLimits) + updateLimits(); + }, [activeUnit, yLimits, plotState.fftLimits]); + + const zoomWindow = React.useMemo(() => { + if (xScaleRef.current == null || yScaleRef.current == null) return null; + const scales = getScales(); + return computeZoomWindow( + d => (scales.x(d) ?? 0) + scales.x.bandwidth() / 2, + scales.y[primaryAxis], + hover, + pointMouse, + mouseMode, + zoomMode, + mouseDown, + plotState.fftLimits[0], + plotState.fftLimits[1], + props.height + ); + }, [hover, pointMouse, mouseDown, mouseMode, zoomMode, plotState.fftLimits, primaryAxis, props.height]); + + React.useEffect(() => { + updateBarColors(containerRef.current, colors); + updateAnglePointColors(containerRef.current, colors); + }, [colors]); + + React.useEffect(() => { + d3.select(containerRef.current).select('svg.root').select('g.root').remove(); + + if (loading === 'Loading' || barData?.length === 0) { + setCreated(false); + return; + } + + rebuildPlot(); + setCreated(true); + }, [props.dataKey, options]); + + React.useEffect(() => { + updateYAxisLabels(containerRef.current, relevantUnits, yLabels, yLblFontSize); + }, [yLabels, yLblFontSize]); + + return ( + <> + 0} + hasTrace={Object.values(enabledBar).some(v => v)} + zoomWindow={zoomWindow} + /> + {loading === 'Loading' || barData?.length === 0 ? null : + + } + + ); +} + +export default BarChart; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/AnglePoints.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/AnglePoints.ts new file mode 100644 index 00000000..f57eea2c --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/AnglePoints.ts @@ -0,0 +1,161 @@ +//****************************************************************************************************** +// AnglePoints.ts - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; +import { seriesToKey } from "../../../Context/PlotKeys"; +import { OpenSee } from "../../../global"; +import { getFactor, IBarScales, toBarSeries } from "../Utils"; + +export const createBarLineGen = ( + scales: IBarScales, + activeUnit: Partial> | null, + unit: OpenSee.Unit | null, + base: number | null +) => { + const yScale = scales.y[unit ?? ""]; + const factor = unit != null && base != null ? getFactor(activeUnit, unit, base) : 1.0; + + return d3.line<[number, number]>() + .x(d => { + const x = scales.x(d[0]); + return x == null ? 0 : x + scales.x.bandwidth() / 2; + }) + .y(d => yScale != null ? yScale(d[1] * factor) : 0) + .defined(d => { + const x = scales.x(d[0]); + const y = yScale != null ? yScale(d[1] * factor) : NaN; + return x != null && Number.isFinite(x) && Number.isFinite(y); + }); +} + +export const drawAnglePoints = ( + container: HTMLDivElement | null, + barData: OpenSee.iD3DataSeries[], + scales: IBarScales, + colors: OpenSee.IColorCollection, + enabledBar: Record, + activeUnit: Partial> | null +) => { + if (container == null) return; + + const pointData = barData.filter(d => d.LegendHorizontal === "Ang"); + const dataContainer = d3.select(container).select(".DataContainer"); + + const points = dataContainer.selectAll(".Point").data(pointData); + const pointEnter = points.enter() + .append("g") + .classed("Point", true) + .attr("fill", d => colors[d.Color] ?? colors.random); + + pointEnter.selectAll("circle") + .data(d => toBarSeries(d, enabledBar)) + .enter() + .append("circle") + .attr("r", 5) + .attr("stroke", "none"); + + points.exit().remove(); + + const lines = dataContainer.selectAll(".Line").data(pointData); + lines.enter() + .append("path") + .classed("Line", true) + .attr("type", d => `axis-${d.Unit}`) + .attr("fill", "none") + .attr("stroke", d => colors[d.Color] ?? colors.random) + .attr("stroke-dasharray", d => d.LineType == null || d.LineType === "-" ? 0 : 5); + + lines.exit().remove(); + updateAnglePointGeometry(container, scales, activeUnit); +} + +export const updateAnglePointGeometry = ( + container: HTMLDivElement | null, + scales: IBarScales, + activeUnit: Partial> | null +) => { + if (container == null) return; + + const dataContainer = d3.select(container).select(".DataContainer"); + + dataContainer + .selectAll(".Point") + .selectAll("circle") + .attr("cx", d => { + const x = scales.x(d.data[0]); + return x == null || isNaN(x) ? 0 : x + scales.x.bandwidth() / 2; + }) + .style("opacity", d => { + const x = scales.x(d.data[0]); + return x == null || isNaN(x) ? 0.0 : 1.0; + }) + .attr("cy", d => { + const yScale = scales.y[d.unit]; + const y = yScale == null ? NaN : yScale(d.data[1] * getFactor(activeUnit, d.unit, d.base)); + return isNaN(y) ? -1 : y; + }) + .attr("r", d => { + const x = scales.x(d.data[0]); + return x == null || isNaN(x) ? 0.0 : 5; + }); + + dataContainer + .selectAll(".Line") + .attr("d", d => { + const lineGen = createBarLineGen(scales, activeUnit, d.Unit, d.BaseValue); + if (d.SmoothDataPoints.length > 0) + return lineGen.curve(d3.curveNatural)(d.SmoothDataPoints); + return lineGen(d.DataPoints); + }); +} + +export const updateAnglePointColors = (container: HTMLDivElement | null, colors: OpenSee.IColorCollection) => { + if (container == null) return; + + const dataContainer = d3.select(container).select(".DataContainer"); + dataContainer.selectAll(".Point") + .attr("fill", d => colors[d.Color] ?? colors.random); + dataContainer.selectAll(".Line") + .attr("stroke", d => colors[d.Color] ?? colors.random); +} + +export const updateAnglePointVisibility = ( + container: HTMLDivElement | null, + barData: OpenSee.iD3DataSeries[], + enabledBar: Record +) => { + if (container == null) return; + + const pointData = barData.filter(d => d.LegendHorizontal === "Ang"); + const dataContainer = d3.select(container).select(".DataContainer"); + + dataContainer.selectAll(".Point") + .data(pointData) + .classed("active", d => enabledBar[seriesToKey(d)] === true) + .style("opacity", d => enabledBar[seriesToKey(d)] === true ? 1.0 : 0); + + dataContainer.selectAll(".Line") + .data(pointData) + .classed("active", d => enabledBar[seriesToKey(d)] === true) + .style("opacity", d => enabledBar[seriesToKey(d)] === true ? 1.0 : 0); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/Bars.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/Bars.ts new file mode 100644 index 00000000..1bea0a0f --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/Bars.ts @@ -0,0 +1,115 @@ +//****************************************************************************************************** +// Bars.ts - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; +import { seriesToKey } from "../../../Context/PlotKeys"; +import { OpenSee } from "../../../global"; +import { getFactor, IBarScales, toBarSeries } from "../Utils"; + +export const drawBars = ( + container: HTMLDivElement | null, + barData: OpenSee.iD3DataSeries[], + scales: IBarScales, + colors: OpenSee.IColorCollection, + enabledBar: Record, + activeUnit: Partial> | null, + plotHeight: number +) => { + if (container == null) return; + + const rectData = barData.filter(d => d.LegendHorizontal === "Mag"); + const bars = d3.select(container).select(".DataContainer").selectAll(".Bar").data(rectData); + + const enter = bars.enter() + .append("g") + .classed("Bar", true) + .attr("stroke", d => colors[d.Color] ?? colors.random); + + enter.selectAll("rect") + .data(d => toBarSeries(d, enabledBar)) + .enter() + .append("rect") + .attr("fill", "none") + .attr("stroke-width", 2); + + bars.exit().remove(); + updateBarGeometry(container, scales, activeUnit, plotHeight); +} + +export const updateBarGeometry = ( + container: HTMLDivElement | null, + scales: IBarScales, + activeUnit: Partial> | null, + plotHeight: number +) => { + if (container == null) return; + + d3.select(container) + .select(".DataContainer") + .selectAll(".Bar") + .selectAll("rect") + .attr("x", d => { + const x = scales.x(d.data[0]); + return x == null || isNaN(x) ? 0 : x; + }) + .style("opacity", d => { + const x = scales.x(d.data[0]); + return x == null || isNaN(x) ? 0.0 : 1.0; + }) + .attr("y", d => { + const yScale = scales.y[d.unit]; + const y = yScale == null ? NaN : yScale(d.data[1] * getFactor(activeUnit, d.unit, d.base)); + return isNaN(y) ? 0 : y; + }) + .attr("width", Math.max(scales.x.bandwidth(), 0)) + .attr("height", d => { + const yScale = scales.y[d.unit]; + const y = yScale == null ? NaN : yScale(d.data[1] * getFactor(activeUnit, d.unit, d.base)); + return isNaN(y) ? 0 : Math.max(plotHeight - y, 0); + }); +} + +export const updateBarColors = (container: HTMLDivElement | null, colors: OpenSee.IColorCollection) => { + if (container == null) return; + + d3.select(container) + .select(".DataContainer") + .selectAll(".Bar") + .attr("stroke", d => colors[d.Color] ?? colors.random); +} + +export const updateBarVisibility = ( + container: HTMLDivElement | null, + barData: OpenSee.iD3DataSeries[], + enabledBar: Record +) => { + if (container == null) return; + + const rectData = barData.filter(d => d.LegendHorizontal === "Mag"); + d3.select(container) + .select(".DataContainer") + .selectAll(".Bar") + .data(rectData) + .classed("active", d => enabledBar[seriesToKey(d)] === true) + .style("opacity", d => enabledBar[seriesToKey(d)] === true ? 1.0 : 0); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/CreateBarPlotFrame.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/CreateBarPlotFrame.ts new file mode 100644 index 00000000..02f9bb97 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/CreateBarPlotFrame.ts @@ -0,0 +1,68 @@ +//****************************************************************************************************** +// PlotChromeBar.ts - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; +import { OpenSee } from "../../../global"; +import { GetDisplayLabel } from "../../Utils/Utilities"; +import { CreatePlotSVG, IChromeHandlers } from "../../Renderers/CreatePlotSVG"; +import { createYAxes } from "../../Renderers/YAxes"; +import { createFrequencyXAxis, setFrequencyDomainFromBands } from "./XAxisFreq"; + +export interface IBarPlotChromeParams { + height: number; + width: number; + dataKey: OpenSee.IGraphProps; + yLimits: Partial>; + enabledUnits: OpenSee.Unit[]; + yLabels: Partial>; + barData: OpenSee.iD3DataSeries[]; + fftLimits: [number, number]; +} + +export const CreateBarPlotFrame = ( + containerEl: HTMLDivElement | null, + xScaleRef: { current: d3.ScaleBand }, + xScaleLblRef: { current: d3.ScaleLinear }, + yScaleRef: { current: Record> }, + params: IBarPlotChromeParams, + handlers: IChromeHandlers +) => { + const { height, width, dataKey, yLimits, enabledUnits, yLabels, barData, fftLimits } = params; + const svg = CreatePlotSVG( + containerEl, + yScaleRef, + { height, width, dataKey, yLimits, displayLabel: GetDisplayLabel(dataKey.DataType) }, + handlers + ); + if (svg == null) return; + + const domain = (barData?.[0]?.DataPoints ?? []) + .filter(pt => pt[0] >= fftLimits[0] && pt[0] <= fftLimits[1]) + .map(pt => pt[0]); + + xScaleRef.current = d3.scaleBand(domain, [60, width - 110]); + setFrequencyDomainFromBands(xScaleRef.current, xScaleLblRef.current); + + createFrequencyXAxis(svg, xScaleRef.current, xScaleLblRef.current, height, width); + createYAxes(svg, enabledUnits, yScaleRef.current, yLabels, height, width); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/XAxisFreq.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/XAxisFreq.ts new file mode 100644 index 00000000..412842a2 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Renderers/XAxisFreq.ts @@ -0,0 +1,160 @@ +//****************************************************************************************************** +// XAxisFreq.ts - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; + +export const formatFrequencyTick = (d: number, xScaleLbl: d3.ScaleLinear): string => { + let h = 1; + if (xScaleLbl != null) + h = xScaleLbl.domain()[1] - xScaleLbl.domain()[0]; + + if (h > 100) + return d.toFixed(0); + if (h > 10) + return d.toFixed(1); + if (h > 1) + return d.toFixed(2); + return d.toFixed(3); +} + +export const computeBandOffsets = (xScaleBand: d3.ScaleBand) => { + const step = xScaleBand.step(); + const bandwidth = xScaleBand.bandwidth(); + + const offsetLeft = Number.isFinite(step) && Number.isFinite(bandwidth) + ? step * xScaleBand.paddingOuter() * xScaleBand.align() * 2 + 0.5 * bandwidth + : 0; + + const offsetRight = Number.isFinite(step) && Number.isFinite(bandwidth) + ? step * xScaleBand.paddingOuter() * (1 - xScaleBand.align()) * 2 + 0.5 * bandwidth + : 0; + + return { + offsetLeft, + offsetRight + }; +} + +export const syncFrequencyScaleRange = ( + xScaleBand: d3.ScaleBand, + xScaleLbl: d3.ScaleLinear, + width: number +) => { + const { offsetLeft, offsetRight } = computeBandOffsets(xScaleBand); + xScaleLbl.range([60 + offsetLeft, width - 110 - offsetRight]); + return { + offsetLeft, + offsetRight + }; +} + +export const setFrequencyDomainFromBands = (xScaleBand: d3.ScaleBand, xScaleLbl: d3.ScaleLinear) => { + const domain = xScaleBand.domain(); + if (domain.length === 0) { + xScaleLbl.domain([0, 1]); + return; + } + xScaleLbl.domain([60.0 * domain[0], 60.0 * domain[domain.length - 1]]); +} + +export const createFrequencyXAxis = ( + svg: d3.Selection, + xScaleBand: d3.ScaleBand, + xScaleLbl: d3.ScaleLinear, + height: number, + width: number +) => { + syncFrequencyScaleRange(xScaleBand, xScaleLbl, width); + + svg.append("g") + .classed("xAxis", true) + .attr("transform", `translate(0,${height - 40})`) + .call(d3.axisBottom(xScaleLbl).tickFormat(d => formatFrequencyTick(d as number, xScaleLbl)).tickSizeOuter(0)); + + svg.append("text").classed("xAxisLabel", true) + .attr("transform", `translate(${(width - 210) / 2 + 60},${height - 10})`) + .style("text-anchor", "middle") + .text("Harmonic (Hz)"); + + updateFrequencyAxisExtents(svg, xScaleBand, xScaleLbl, height, width); +} + +export const updateFrequencyXAxisTicks = (container: HTMLDivElement | null, xScaleLbl: d3.ScaleLinear) => { + if (container == null) return; + + d3.select(container) + .selectAll(".xAxis") + .transition() + .call(d3.axisBottom(xScaleLbl).tickFormat(d => formatFrequencyTick(d as number, xScaleLbl)).tickSizeOuter(0) as any); +} + +export const updateFrequencyXAxisOnResize = ( + container: HTMLDivElement | null, + xScaleBand: d3.ScaleBand, + xScaleLbl: d3.ScaleLinear, + height: number, + width: number +) => { + if (container == null) return; + + const sel = d3.select(container); + syncFrequencyScaleRange(xScaleBand, xScaleLbl, width); + + sel.select(".xAxis").attr("transform", `translate(0,${height - 40})`); + sel.select(".xAxisLabel").attr("transform", `translate(${(width - 210) / 2 + 60},${height - 10})`); + sel.select(".plotTitle").attr("transform", `translate(${(width - 210) / 2 + 60},20)`).style("font-weight", "bold"); + + updateFrequencyAxisExtents(sel.select("g.root") as any, xScaleBand, xScaleLbl, height, width); + updateFrequencyXAxisTicks(container, xScaleLbl); +} + +const updateFrequencyAxisExtents = ( + svg: d3.Selection, + xScaleBand: d3.ScaleBand, + xScaleLbl: d3.ScaleLinear, + height: number, + width: number +) => { + const { offsetLeft, offsetRight } = syncFrequencyScaleRange(xScaleBand, xScaleLbl, width); + const left = svg.selectAll(".xAxisExtLeft").data([null]); + left.enter() + .append("line") + .classed("xAxisExtLeft", true) + .attr("stroke", "currentColor") + .merge(left) + .attr("x1", 60) + .attr("x2", 60 + offsetLeft) + .attr("y1", height - 40) + .attr("y2", height - 40); + + const right = svg.selectAll(".xAxisExtRight").data([null]); + right.enter() + .append("line") + .classed("xAxisExtRight", true) + .attr("stroke", "currentColor") + .merge(right) + .attr("x1", width - 110) + .attr("x2", width - 110 - offsetRight) + .attr("y1", height - 40) + .attr("y2", height - 40); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Utils.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Utils.ts new file mode 100644 index 00000000..9ddf4c77 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/BarChart/Utils.ts @@ -0,0 +1,121 @@ +//****************************************************************************************************** +// Utils.ts - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; +import React from "react"; +import { PlotDataMap, PlotStateActionContext } from "../../Context/PlotStateContext"; +import { OpenSee } from "../../global"; +import { usePanZoomInteractions } from "../Utils/Utilities"; +import { seriesToKey } from "../../Context/PlotKeys"; + +export const getXbucket = (pixel: number, xScale: d3.ScaleBand): number => { + const domain = xScale.domain(); + if (domain.length === 0) + return NaN; + + const range = xScale.range(); + const step = xScale.step(); + if (!Number.isFinite(step) || step <= 0) + return domain[0]; + + const p = pixel - range[0]; + let index = Math.floor(p / step); + + if (index < 0) + index = 0; + if (index >= domain.length) + index = domain.length - 1; + + return domain[index]; +} + +interface IInputs { + containerRef: React.MutableRefObject; + xScaleRef: React.MutableRefObject>; + yScaleRef: React.MutableRefObject> | {}>; + primaryAxis: string; + hover: [number, number]; + setHover: (h: [number, number]) => void; + mouseMode: OpenSee.MouseMode; + zoomMode: OpenSee.ZoomMode; + plotData: PlotDataMap; + dataKey: OpenSee.IGraphProps; + height: number; + width: number; + fftLimits: [number, number]; + yLimits: Partial>; +} + +export const useBarMouseInteractions = (inputs: IInputs) => { + const stateActions = React.useContext(PlotStateActionContext); + const { + containerRef, xScaleRef, yScaleRef, primaryAxis, + hover, setHover, mouseMode, zoomMode, plotData, + dataKey, height, width, fftLimits, yLimits + } = inputs; + + return usePanZoomInteractions({ + containerRef, + yScaleRef, + primaryAxis, + hover, + setHover, + mouseMode, + zoomMode, + plotData, + dataKey, + height, + width, + xDomainStart: fftLimits[0], + xDomainEnd: fftLimits[1], + yLimits, + pxToDomainX: px => getXbucket(px, xScaleRef.current), + pxAtDomainX: d => (xScaleRef.current(d) ?? 0) + xScaleRef.current.bandwidth() / 2, + setXLimits: stateActions.SetFFTLimits, + setYLimits: stateActions.SetZoomedLimits + }); +} + +export interface IBarScales { + x: d3.ScaleBand; + y: Record>; +} + +export const getFactor = (activeUnit: Partial> | null, unit: OpenSee.Unit, base: number): number => { + const factor = activeUnit?.[unit]?.factor; + if (factor === undefined && activeUnit?.[unit] != null) + return 1.0 / base; + return factor ?? 1.0; +} + +export const toBarSeries = (d: OpenSee.iD3DataSeries, enabledBar: Record): OpenSee.BarSeries[] => { + const seriesKey = seriesToKey(d); + return d.DataPoints.map(pt => ({ + unit: d.Unit, + data: pt, + color: d.Color, + base: d.BaseValue, + enabled: enabledBar[seriesKey] === true, + seriesKey + })); +} \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/ChartContainer.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/ChartContainer.tsx new file mode 100644 index 00000000..27839a2c --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/ChartContainer.tsx @@ -0,0 +1,85 @@ +//****************************************************************************************************** +// ChartContainer.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as React from 'react'; +import { OpenSee } from '../global'; +import { ErrorIcon, LoadingIcon, NoDataIcon } from './ChartIcons'; +import PolyLine, { PolyLineSpec } from './PolyLine'; +import { ZoomWindowRect } from './Renderers/ZoomWindow'; + +interface IContainerProps { + height: number; + dataKey: OpenSee.IGraphProps; + loading: OpenSee.LoadingState; + hasData: boolean; + hasTrace: boolean; + polyLines?: PolyLineSpec[]; + zoomWindow?: ZoomWindowRect | null; +} + +const ChartContainer = React.memo(React.forwardRef((props, ref) => { + const showSVG = props.loading != 'Loading' && props.hasData; + + return ( +
    + {props.loading === 'Loading' ? : null} + {props.loading != 'Loading' && props.loading != 'Error' && !props.hasData ? : null} + {props.loading === 'Error' ? : null} + + {/* Data SVG - D3 draws axes and the trace paths into this element. */} + + {props.loading != 'Loading' && props.hasData && !props.hasTrace ? + Select a Trace in the Legend to Display. + : null} + + + {/* Hover/marker overlay in a separate SVG layered on top, so moving the hover line only repaints these thin lines. */} + {showSVG ? ( + + + {props.zoomWindow != null ? ( + + ) : null} + {(props.polyLines ?? []).map(line => ( + + ))} + + + ) : null} +
    + ); +})); + +export default ChartContainer; diff --git a/src/OpenSEE/Scripts/TSX/Graphs/ChartIcons.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/ChartIcons.tsx similarity index 80% rename from src/OpenSEE/Scripts/TSX/Graphs/ChartIcons.tsx rename to src/OpenSEE/wwwroot/Scripts/TSX/Graphs/ChartIcons.tsx index 7f642976..cff5bbcf 100644 --- a/src/OpenSEE/Scripts/TSX/Graphs/ChartIcons.tsx +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/ChartIcons.tsx @@ -62,30 +62,4 @@ export const ErrorIcon = () => { ); -} - -export const WaveformViews = '👁'; -export const ShowPoints = '✏'; -export const CorrelatedSags = '📈'; -export const PhasorClock = '⟴'; -export const statsIcon = 'ℹ'; -export const lightningData = '⚡'; -export const exportBtn = '💾'; - - - -export const Zoom = '🔍'; -export const Pan = '🖐'; -export const FFT = '📊'; -export const Reset = '↻'; - -export const Settings = '⚙'; -export const leftArrow = '⏮'; -export const rightArrow = '⏭'; -export const Help = '❔'; - - -export const WarningSymbol = '⚠'; -export const Square = '⇄⇅'; -export const ValueRect = '⇅'; -export const TimeRect = '⇄'; +} \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/DigitalLegend.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/DigitalLegend.tsx new file mode 100644 index 00000000..fbbdaec2 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/DigitalLegend.tsx @@ -0,0 +1,124 @@ +//****************************************************************************************************** +// DigitalLegend.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 06/01/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** +import * as React from "react"; +import { Alert, OverlayDrawer } from "@gpa-gemstone/react-interactive"; +import { MultiCheckBoxSelect, SearchableSelect } from "@gpa-gemstone/react-forms"; +import { sortHorizontal } from './Utilities'; +import { useLegendGrid, rowKey } from './useLegendGrid'; +import Header from './Header'; +import Row from './Row'; +import { OpenSee } from '../../global'; + +const maxInitialRows = 10; + +interface IProps { + height: number, + dataKey: OpenSee.IGraphProps +} + +const DigitalLegend = (props: IProps) => { + const { grid, categories, verticalHeader, horizontalHeader, changeCategory, clickGroup, toggleTrace } = useLegendGrid(props.dataKey); + + const [addedKeys, setAddedKeys] = React.useState([]); + const hasSelectedAssets = categories.some(c => c.Selected); + + const visibleHeader = React.useMemo(() => { + if (verticalHeader.length <= maxInitialRows) return verticalHeader; + return verticalHeader.filter((v, i) => + i < maxInitialRows + || (grid.get(rowKey(v))?.some(g => g.enabled) ?? false) + || addedKeys.includes(rowKey(v)) + ); + }, [verticalHeader, grid, addedKeys]); + + const hasHidden = verticalHeader.length > visibleHeader.length; + + const hwidth = (200 - 4) / (horizontalHeader.length + (verticalHeader.length > 1 ? 2 : 1)); + + const search = (text: string) => { + const visibleKeys = new Set(visibleHeader.map(rowKey)); + const opts = verticalHeader + .filter(v => !visibleKeys.has(rowKey(v))) + .filter(v => v[0].toLowerCase().includes(text.toLowerCase())) + .map(v => ({ Label: v[0], Value: rowKey(v) })); + return Promise.resolve(opts); + }; + + return ( + +
    +
    + changeCategory(options)} + Label="" + /> +
    + {hasSelectedAssets && hasHidden ? +
    + + Record={{search: ''}} + Field={'search'} + Label="Add Channel" + Help="Search for channels to add to the legend." + AllowCustom={false} + ResetSearchOnSelect={true} + Search={search} + Style={{ width: '100%' }} + BtnStyle={{ display: 'flex', paddingLeft: 0, alignItems: 'center' }} + Setter={(_, opt) => setAddedKeys(prev => prev.includes(opt.Value as string) ? prev : [...prev, opt.Value as string])} + /> +
    : null} + {hasSelectedAssets ? +
    +
    +
    1 ? 2 : 1) * hwidth, backgroundColor: "#b2b2b2" }} /> + {horizontalHeader.map((item, i) => +
    clickGroup(g, t, new Set(visibleHeader.map(rowKey)))} + /> + )} +
    + {visibleHeader.map((v, i) => ( + sortHorizontal(a.hLabel, b.hLabel)) ?? []} + width={hwidth} + clickHeader={clickGroup} + horizontalHeaders={horizontalHeader} + toggleTrace={toggleTrace} + /> + ))} +
    : + Please select an asset. + } +
    + + ); +}; + +export default DigitalLegend; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Header.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Header.tsx new file mode 100644 index 00000000..72b5021e --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Header.tsx @@ -0,0 +1,41 @@ +//****************************************************************************************************** +// Header.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as React from "react"; +import { LegendGroupType } from './Types'; + +interface IProps { + label: string, + width: number, + onClick: (s: string, t: LegendGroupType) => void +} + +const Header = (props: IProps) => ( +
    + props.onClick(props.label, 'horizontal')}> + {props.label} + +
    +); + +export default Header; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Legend.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Legend.tsx new file mode 100644 index 00000000..ff8755b4 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Legend.tsx @@ -0,0 +1,122 @@ +//****************************************************************************************************** +// LegendBase.tsx - 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 01/06/2020 - C Lackner +// Generated original version of source code. +// 07/08/2020 - C Lackner +// Refactored Trace Picker to work as Grid. +// +//****************************************************************************************************** +import * as React from "react"; +import { Alert, OverlayDrawer } from "@gpa-gemstone/react-interactive"; +import { MultiCheckBoxSelect } from "@gpa-gemstone/react-forms"; +import { sortGroup, sortHorizontal, uniq } from './Utilities'; +import { useLegendGrid, rowKey } from './useLegendGrid'; +import DigitalLegend from './DigitalLegend'; +import Header from './Header'; +import Row from './Row'; +import VCategory from './VCategory'; +import { OpenSee } from '../../global'; + +const hrow = 26; + +interface IProps { + height: number, + dataKey: OpenSee.IGraphProps +} + +const Legend = (props: IProps) => { + if (props.dataKey.DataType === 'Digitals') + return ; + + return ; +}; + +const StandardLegend = (props: IProps) => { + const { grid, categories, verticalHeader, horizontalHeader, changeCategory, clickGroup, toggleTrace } = useLegendGrid(props.dataKey); + + const hwidth = (200 - 4) / (horizontalHeader.length + (verticalHeader.length > 1 ? 2 : 1)); + const hasSelectedAssets = categories.some(c => c.Selected); + + return ( + +
    +
    + changeCategory(options)} + Label="" + /> +
    + {hasSelectedAssets ? +
    +
    +
    1 ? 2 : 1) * hwidth, backgroundColor: "#b2b2b2" }} /> + {horizontalHeader.map((item, i) => +
    + )} +
    +
    + {verticalHeader.length > 1 && verticalHeader.some(v => v[1]) ? +
    + {uniq(verticalHeader, v => v[1]).sort(sortGroup).map((v, i) => + item[1] == v[1]).length} + width={hwidth} + /> + )} +
    : null} +
    1 && verticalHeader.some(v => v[1]) ? `calc(100% - ${hwidth}px)` : "100%", + backgroundColor: "rgb(204,204,204)", + overflow: "hidden", + textAlign: "center", + display: "inline-block", + verticalAlign: "top" + }}> + {verticalHeader.map((v, i) => ( + sortHorizontal(a.hLabel, b.hLabel)) ?? []} + width={hwidth} + clickHeader={clickGroup} + horizontalHeaders={horizontalHeader} + toggleTrace={toggleTrace} + /> + ))} +
    +
    +
    : + + Please select an asset. + + } +
    + + ); +}; + +export default Legend; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Row.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Row.tsx new file mode 100644 index 00000000..59ad71ed --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Row.tsx @@ -0,0 +1,63 @@ +//****************************************************************************************************** +// Row.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as React from "react"; +import { LegendTraceKey } from '../../Context/PlotKeys'; +import { ILegendGrid, LegendGroupType } from './Types'; +import TraceButton from './TraceButton'; + +interface IProps { + category: string, + label: string, + data: ILegendGrid[], + width: number, + clickHeader: (g: string, t: LegendGroupType) => void, + horizontalHeaders: string[], + toggleTrace: (traceKey: LegendTraceKey) => void +} + +const Row = (props: IProps) => { + const hasH = props.horizontalHeaders.some(h => h); + const hasCat = props.category !== '' && props.category !== null; + const labelWidth = !hasH && !hasCat ? '50%' : hasH && !hasCat ? 2 * props.width : props.width; + + return ( +
    +
    + props.clickHeader(props.label + props.category, 'vertical')}> + {props.label} + +
    + {props.data.map((item, i) => + + )} +
    + ); +}; + +export default Row; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/TraceButton.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/TraceButton.tsx new file mode 100644 index 00000000..95e58881 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/TraceButton.tsx @@ -0,0 +1,54 @@ +//****************************************************************************************************** +// TraceButton.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as React from "react"; +import { OpenSee } from '../../global'; +import { LegendTraceKey } from '../../Context/PlotKeys'; +import { useAppSelector } from '../../hooks'; +import { SelectColor } from '../../Store/settingSlice'; +import { ILegendGrid } from './Types'; +import { convertHex } from './Utilities'; +import { ReactIcons } from "@gpa-gemstone/gpa-symbols"; + +const TraceButton = (props: { data: ILegendGrid, width: React.CSSProperties, onToggle: (traceKey: LegendTraceKey) => void }) => { + const colors = useAppSelector(SelectColor); + + const getColor = (color: OpenSee.Color) => { + return Object.keys(colors).includes(color as string) ? colors[color] : colors.random; + }; + + return ( +
    props.onToggle(props.data.traceKey)} + > + {props.data.enabled ? + : + + } +
    + ); +}; + +export default TraceButton; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Types.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Types.ts new file mode 100644 index 00000000..bac53a5b --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Types.ts @@ -0,0 +1,30 @@ +//****************************************************************************************************** +// Types.ts - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** +import { OpenSee } from '../../global'; +import { SeriesKey, LegendTraceKey } from '../../Context/PlotKeys'; + +export interface ILegendGrid { enabled: boolean, hLabel: string, vLabel: string, traceKey: LegendTraceKey, color: OpenSee.Color, traces: Map, category?: string } +export type LegendGroupType = 'vertical' | 'horizontal'; + +//This should really be exported via gpa-gemstone +export interface ICategory { Value: number, Label: string, Selected: boolean } diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Utilities.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Utilities.ts new file mode 100644 index 00000000..2e5d3d1f --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/Utilities.ts @@ -0,0 +1,72 @@ +//****************************************************************************************************** +// Utilities.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** +import { ILegendGrid } from './Types'; + +const horizontalSort = ['W', 'Pk', 'RMS', 'Ph', 'V', 'I', 'Pre', 'Post', 'P', 'Q', 'S', 'PF', 'R', 'X', 'Z', 'Mag', 'Ang']; +const verticalGroupSort = ['L-N', 'L-L', 'Volt.', 'Curr.', 'V', 'I']; +const verticalSort = ['AN', 'BN', 'CN', 'NG', 'RES', 'AB', 'BC', 'CA', 'Avg', 'Total', 'Pos', 'Neg', 'Zero', 'Zero/Pos', 'Neg/Pos', 'Simple', 'Reactance', 'Takagi', 'ModifiedTakagi', 'Novosel', 'DoubleEnded']; + +export const sortHorizontal = (a: string, b: string): number => { + if (a == b) return 0; + const ia = horizontalSort.indexOf(a), ib = horizontalSort.indexOf(b); + if (ia != -1 && ib != -1) return ia - ib; + if (ia != -1) return 1; + if (ib != -1) return -1; + return a > b ? 1 : -1; +}; + +export const sortVertical = (a: [string, string], b: [string, string]): number => { + if (a[1] != b[1]) return sortGroup(a, b); + if (a[0] == b[0]) return 0; + const ia = verticalSort.indexOf(a[0]), ib = verticalSort.indexOf(b[0]); + if (ia != -1 && ib != -1) return ia - ib; + if (ia != -1) return 1; + if (ib != -1) return -1; + return a[0] > b[0] ? 1 : -1; +}; + +export const sortGroup = (a: [string, string], b: [string, string]): number => { + if (a[1] == b[1]) return 0; + const ia = verticalGroupSort.indexOf(a[1]), ib = verticalGroupSort.indexOf(b[1]); + if (ia != -1 && ib != -1) return ia - ib; + if (ia != -1) return 1; + if (ib != -1) return -1; + return a[1] > b[1] ? 1 : -1; +}; + +export const uniq = (array: T[], fx: (item: T) => string): T[] => { + const result: T[] = [], seen: string[] = []; + array.forEach(item => { const k = fx(item); if (!seen.includes(k)) { result.push(item); seen.push(k); } }); + return result; +}; + +export const groupBy = (list: ILegendGrid[], fnct: (v: ILegendGrid) => string): Map => { + const result = new Map(); + list.forEach(item => { const k = fnct(item); const v = result.get(k); if (v) v.push(item); else result.set(k, [item]); }); + return result; +}; + +export const convertHex = (hex: string, opacity: number) => { + hex = hex.replace("#", ""); + return `rgba(${parseInt(hex.substring(0, 2), 16)},${parseInt(hex.substring(2, 4), 16)},${parseInt(hex.substring(4, 6), 16)},${opacity / 100})`; +}; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/VCategory.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/VCategory.tsx new file mode 100644 index 00000000..1432b8cd --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/VCategory.tsx @@ -0,0 +1,39 @@ +//****************************************************************************************************** +// VCategory.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** +import * as React from "react"; + +interface IProps { + label: string, + height: number, + width: number +} + +const VCategory = (props: IProps) => ( +
    + + {props.label} + +
    +); + +export default VCategory; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/useLegendGrid.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/useLegendGrid.ts new file mode 100644 index 00000000..83c0e97b --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Legend/useLegendGrid.ts @@ -0,0 +1,134 @@ +//****************************************************************************************************** +// useLegendGrid.ts - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 06/01/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** +import * as React from "react"; +import { PlotDataStateContext } from "../../Context/PlotDataContext"; +import { PlotStateStateContext, PlotStateActionContext } from "../../Context/PlotStateContext"; +import { LegendTraceKey, seriesToKey, seriesToLegendTraceKey, toPlotKey } from "../../Context/PlotKeys"; +import { ICategory, ILegendGrid, LegendGroupType } from './Types'; +import { sortHorizontal, uniq, groupBy, sortVertical } from './Utilities'; +import { OpenSee } from '../../global'; + +// Key for a verticalHeader entry ([vLabel, category]) +export const rowKey = (v: [string, string]) => v[0] + v[1]; + +type CategoryOption = { Label: string | JSX.Element }; + +export const useLegendGrid = (dataKey: OpenSee.IGraphProps) => { + const { plots } = React.useContext(PlotDataStateContext); + const plotState = React.useContext(PlotStateStateContext); + const stateActions = React.useContext(PlotStateActionContext); + + const pk = toPlotKey(dataKey); + const dataPoints = plots[pk] ?? []; + const selectedAssets = plotState.meta[pk]?.selectedAssets ?? []; + const selectedTraces = plotState.meta[pk]?.selectedTraces ?? []; + + const [categories, setCategories] = React.useState([]); + const [verticalHeader, setVerticalHeader] = React.useState<[string, string][]>([]); + const [horizontalHeader, setHorizontalHeader] = React.useState([]); + const [grid, setGrid] = React.useState>(new Map()); + + React.useEffect(() => { const id = setTimeout(buildGrid, 250); return () => clearTimeout(id); }, [dataPoints, selectedAssets, selectedTraces]); + + const buildGrid = () => { + const nextGrid = buildLegendGrid(dataPoints, selectedAssets, selectedTraces); + setGrid(nextGrid.grid); + setCategories(nextGrid.categories); + setVerticalHeader(nextGrid.verticalHeader); + setHorizontalHeader(nextGrid.horizontalHeader); + }; + + const changeCategory = (items: CategoryOption[]) => { + const nextAssets = [...selectedAssets]; + items.forEach(item => { + if (typeof item.Label !== 'string') return; + + const index = nextAssets.indexOf(item.Label); + if (index >= 0) + nextAssets.splice(index, 1); + else + nextAssets.push(item.Label); + }); + stateActions.SetLegendSelections(dataKey, dataPoints, nextAssets, selectedTraces); + }; + + const toggleTraceKeys = (traceKeys: LegendTraceKey[]) => { + const nextTraces = [...selectedTraces]; + const isAny = traceKeys.some(traceKey => nextTraces.includes(traceKey)); + + traceKeys.forEach(traceKey => { + const index = nextTraces.indexOf(traceKey); + if (isAny && index >= 0) + nextTraces.splice(index, 1); + else if (!isAny && index < 0) + nextTraces.push(traceKey); + }); + + stateActions.SetLegendSelections(dataKey, dataPoints, selectedAssets, nextTraces); + }; + + // visibleKeys (optional) limits a horizontal-group click to the rows currently shown in the legend. + const clickGroup = (group: string, type: LegendGroupType, visibleKeys?: Set) => { + const inScope = (key: string) => visibleKeys == null || visibleKeys.has(key); + const traceKeys: LegendTraceKey[] = []; + + if (type == 'vertical') { + const gv = grid.get(group); + gv?.forEach(row => { if (!traceKeys.includes(row.traceKey)) traceKeys.push(row.traceKey); }); + } else { + grid.forEach((row, key) => { if (!inScope(key)) return; row.forEach(item => { + if (item.hLabel == group && !traceKeys.includes(item.traceKey)) traceKeys.push(item.traceKey); + }); }); + } + toggleTraceKeys(traceKeys); + }; + + return { dataPoints, grid, categories, verticalHeader, horizontalHeader, changeCategory, clickGroup, toggleTrace: (traceKey: LegendTraceKey) => toggleTraceKeys([traceKey]) }; +}; + +const buildLegendGrid = (dataPoints: OpenSee.iD3DataSeries[], selectedAssets: string[], selectedTraces: LegendTraceKey[]) => { + const cats: ICategory[] = []; + const gridArr: ILegendGrid[] = []; + + dataPoints.forEach((item) => { + const sk = seriesToKey(item); + const traceKey = seriesToLegendTraceKey(item); + let ci = cats.findIndex(c => c.Label === item.LegendGroup); + if (ci === -1) { cats.push({ Value: 0, Label: item.LegendGroup, Selected: false }); ci = cats.length - 1; } + if (selectedAssets.includes(item.LegendGroup)) cats[ci].Selected = true; + + let gi = gridArr.findIndex(g => g.hLabel === item.LegendHorizontal && g.vLabel === item.LegendVertical && g.category == item.LegendVGroup); + if (gi === -1) { gridArr.push({ enabled: false, hLabel: item.LegendHorizontal, vLabel: item.LegendVertical, traceKey, color: item.Color, traces: new Map(), category: item.LegendVGroup }); gi = gridArr.length - 1; } + if (selectedTraces.includes(traceKey)) gridArr[gi].enabled = true; + + const t = gridArr[gi].traces.get(item.LegendGroup); + if (t) t.push(sk); else gridArr[gi].traces.set(item.LegendGroup, [sk]); + }); + + return { + grid: groupBy(gridArr, item => rowKey([item.vLabel, item.category] as [string, string])), + categories: cats, + verticalHeader: uniq(gridArr.map(item => [item.vLabel, item.category] as [string, string]), rowKey).sort(sortVertical), + horizontalHeader: uniq(gridArr.map(item => item.hLabel), d => d).sort(sortHorizontal) + }; +}; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart.tsx new file mode 100644 index 00000000..a0e15d96 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart.tsx @@ -0,0 +1,355 @@ +//****************************************************************************************************** +// LineChartBase.tsx - 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 01/22/2020 - C. Lackner +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; +import * as React from 'react'; +import { AnalyticContext, SelectAnalyticOptions } from '../Context/AnalyticContext'; +import { PlotDataStateContext } from '../Context/PlotDataContext'; +import { PlotStateStateContext } from '../Context/PlotStateContext'; +import { OverlappingStateContext } from '../Context/OverlappingContext'; +import { toPlotKey } from '../Context/PlotKeys'; +import { selectYLimits, selectYLabels, selectActiveUnit, selectRelevantUnits, selectEnabledUnits, selectFFTEnabled, selectDeltaHoverPoints } from '../PlotSelectors'; +import EventContext from '../Context/EventContext'; +import HoverContext from '../Context/HoverContext'; +import { OpenSee } from '../global'; +import { useAppSelector } from '../hooks'; +import { SelectColor, SelectMouseMode, SelectOverlappingWaveTimeUnit, SelectPlotMarkers, SelectSinglePlot, SelectTimeUnit, SelectUseOverlappingTime, SelectZoomMode } from '../Store/settingSlice'; +import Legend from './Legend/Legend'; +import ChartContainer from './ChartContainer'; +import { GetDisplayLabel, useChartScales, useTooltipLocations, useYLabelFontSize } from './Utils/Utilities'; +import { getPrimaryAxis } from '../Context/PlotStateUtilities'; +import { useFFTWindow } from './LineChart/hooks/useFFTWindow'; +import { useTimeFormatContext } from './LineChart/hooks/useTimeFormatContext'; +import { useMouseInteractions } from './LineChart/hooks/useMouseInteractions'; +import { CreateLinePlot } from './LineChart/Renderers/CreatePlot'; +import { drawLines, updateLineGeometry, updateLineColors, updateLineVisibility, IScales } from './LineChart/Renderers/Lines'; +import { drawMarkers, updateMarkerGeometry, updateMarkerColors, updateMarkerVisibility } from './Renderers/Markers'; +import { updateXAxisTicks, updateXAxisLabel, updateXAxisPositionOnResize } from './LineChart/Renderers/XAxes'; +import { updateYAxes, updateYAxisLabels, updateYAxisVisibility, updateYAxisPositionsOnResize } from './Renderers/YAxes'; +import { updateFFTWindow as updateFFTWindowD3, updateDurationWindowRect } from './LineChart/Renderers/Overlays'; +import { computeZoomWindow } from './Renderers/ZoomWindow'; +import { PolyLineSpec } from './PolyLine'; + +interface IProps { + height: number, + width: number, + showToolTip: boolean, + dataKey: OpenSee.IGraphProps +} + +const hoverLineStyle: React.CSSProperties = { stroke: "#000", opacity: 0.5 }; + +const LineChart = (props: IProps) => { + const [hover, setHover] = React.useContext(HoverContext); + const [analytic, setAnalytic] = React.useContext(AnalyticContext); + const evt = React.useContext(EventContext); + const { plots: plotData } = React.useContext(PlotDataStateContext); + const plotState = React.useContext(PlotStateStateContext); + const overlapping = React.useContext(OverlappingStateContext); + const pk = toPlotKey(props.dataKey); + const plotMeta = plotState.meta[pk]; + + const isOverlappingWaveform = props.dataKey.DataType === "OverlappingWave"; + const primaryAxis = getPrimaryAxis(props.dataKey); + const isOriginalEvt = props.dataKey.EventId === evt.Context.EventID; + + const lineData = plotData[pk] ?? []; + const loading = plotMeta?.loading ?? 'Uninitiated'; + const enabledLine = plotMeta?.enabled ?? {}; + const isZoomed = plotMeta?.isZoomed ?? false; + + const activeUnit = React.useMemo(() => plotMeta ? selectActiveUnit(plotMeta) : null, [plotMeta]); + const relevantUnits = React.useMemo(() => selectRelevantUnits(props.dataKey, lineData), [lineData]); + const enabledUnits = React.useMemo(() => selectEnabledUnits(props.dataKey, lineData, plotMeta?.enabled ?? {}), [lineData, plotMeta?.enabled]); + const yLimits = React.useMemo(() => plotMeta ? selectYLimits(plotMeta) : {} as any, [plotMeta]); + const yLabels = React.useMemo(() => plotMeta ? selectYLabels(plotMeta) : {} as any, [plotMeta]); + const options = React.useMemo(() => SelectAnalyticOptions(analytic, props.dataKey.DataType), [analytic, props.dataKey]); + const fftWindow = React.useMemo(() => ([analytic.FFTStartTime, analytic.FFTStartTime + (analytic.FFTCycles * 1 / 60.0 * 1000.0)] as [number, number]), [analytic]); + const showFFT = React.useMemo(() => selectFFTEnabled(plotState.meta), [plotState.meta]); + const points = React.useMemo(() => selectDeltaHoverPoints(hover, props.dataKey.EventId, plotData, plotState.meta, props.dataKey, options?.[0]), [hover, props.dataKey, plotData, plotState.meta, options]); + + const singlePlot = useAppSelector(SelectSinglePlot); + const plotMarkers = useAppSelector(SelectPlotMarkers); + const colors = useAppSelector(SelectColor); + const timeUnit = useAppSelector(SelectTimeUnit); + const overlappingWaveTimeUnit = useAppSelector(SelectOverlappingWaveTimeUnit); + const mouseMode = useAppSelector(SelectMouseMode); + const zoomMode = useAppSelector(SelectZoomMode); + const useRelevantTime = useAppSelector(SelectUseOverlappingTime); + + const startTime = isOverlappingWaveform ? plotState.cycleLimits[0] : plotState.startTime; + const endTime = isOverlappingWaveform ? plotState.cycleLimits[1] : plotState.endTime; + const originalStartTime = new Date(evt.Context.EventInfo?.EventDate + "Z").getTime(); + const inceptionTime = new Date(evt.Context.EventInfo?.InceptionDate + "Z").getTime(); + + const containerRef = React.useRef(null); + const { xScaleRef, yScaleRef } = useChartScales(d3.scaleLinear()); + const firstAnalyticOption = options?.[0]; + const xRange = React.useMemo<[number, number]>(() => { + if (enabledUnits?.length > 2) + return [120, props.width - 110]; + return [60, props.width - 110]; + }, [enabledUnits, props.width]); + + React.useLayoutEffect(() => { + if (xScaleRef.current == null) return; + xScaleRef.current.domain([startTime, endTime]).range(xRange); + }, [startTime, endTime, xRange]); + + const buildTimeCtx = useTimeFormatContext( + xScaleRef, + isOverlappingWaveform, + overlappingWaveTimeUnit, + timeUnit, + originalStartTime, + useRelevantTime, + isOriginalEvt, + overlapping.events, + props.dataKey.EventId, + inceptionTime, + startTime + ); + + const { currentFFTWindow, setCurrentFFTWindow, oldFFTWindow, setOldFFTWindow } = useFFTWindow(xScaleRef, fftWindow); + const yLblFontSize = useYLabelFontSize(yLabels, primaryAxis, props.height); + const { toolTipLocation, selectedPointLocation, inceptionLocation, durationLocation } = useTooltipLocations(xScaleRef, hover, points, evt.Context.EventInfo, startTime, endTime, xRange); + const hoverValueLocation = React.useMemo(() => { + const scale = (yScaleRef.current as Record>)[primaryAxis]; + const location = scale?.(hover[1]); + + return location == null || !Number.isFinite(location) ? null : location; + }, [hover, primaryAxis, activeUnit, yLimits]); + + const { handlers, wheelZoom, mouseDown, pointMouse } = useMouseInteractions({ + containerRef, xScaleRef, yScaleRef, primaryAxis, + hover, setHover, isOverlappingWaveform, + mouseMode, zoomMode, setAnalytic, + plotData, dataKey: props.dataKey, + width: props.width, height: props.height, + fftWindow, startTime, endTime, yLimits, + oldFFTWindow, setOldFFTWindow, setCurrentFFTWindow, + }); + + const [isCreated, setCreated] = React.useState(false); + const lineBottom = props.height - 40; + const hoverLines = React.useMemo(() => { + const lines: PolyLineSpec[] = []; + + if (zoomMode === 'x' || zoomMode === 'xy') + lines.push({ className: 'hoverX', points: `${toolTipLocation},20 ${toolTipLocation},${lineBottom}`, style: hoverLineStyle }); + + if ((zoomMode === 'y' || zoomMode === 'xy') && hoverValueLocation != null) + lines.push({ className: 'hoverY', points: `${xRange[0]},${hoverValueLocation} ${xRange[1]},${hoverValueLocation}`, style: hoverLineStyle }); + + return lines; + }, [zoomMode, toolTipLocation, hoverValueLocation, lineBottom, xRange]); + + const zoomWindow = React.useMemo(() => { + if (xScaleRef.current == null) return null; + const yScale = (yScaleRef.current as Record>)[primaryAxis]; + return computeZoomWindow(d => xScaleRef.current(d), yScale, hover, pointMouse, mouseMode, zoomMode, mouseDown, startTime, endTime, props.height); + }, [hover, pointMouse, mouseDown, mouseMode, zoomMode, startTime, endTime, yLimits, primaryAxis, props.height]); + + const getScales = (): IScales => ({ x: xScaleRef.current, y: yScaleRef.current as Record> }); + + function updateLimits() { + const scales = getScales(); + updateLineGeometry(containerRef.current, enabledLine, scales, activeUnit); + updateMarkerGeometry(containerRef.current, scales, activeUnit); + updateYAxes(containerRef.current, enabledUnits, scales.y, props.width, props.dataKey.DataType, props.dataKey.EventId); + updateXAxisTicks(containerRef.current, scales.x, buildTimeCtx()); + updateXAxisLabel(containerRef.current, scales.x, isOverlappingWaveform, timeUnit); + + if (xScaleRef.current != null && showFFT) + setCurrentFFTWindow([xScaleRef.current(fftWindow[0]), xScaleRef.current(fftWindow[1])]); + } + + // Data load: create plot chrome, draw data, update visibility + React.useEffect(() => { + if (!lineData || lineData.length === 0 || loading === 'Loading') return; + + if (isCreated) { + drawLines(containerRef.current, lineData, enabledLine, getScales(), activeUnit, colors, singlePlot, evt.Context.EventInfo?.EventId ?? null); + drawMarkers(containerRef.current, lineData, getScales(), colors); + } + + CreateLinePlot( + containerRef.current, + xScaleRef, + yScaleRef as { current: Record> }, + { + height: props.height, width: props.width, dataKey: props.dataKey, + yLimits, enabledUnits, yLabels, startTime, endTime, + showFFT, plotMarkers, fftWindow, currentFFTWindow, mouseMode, + timeCtx: buildTimeCtx(), displayLabel: GetDisplayLabel(props.dataKey.DataType, firstAnalyticOption), + eventInfo: evt.Context.EventInfo, + }, + { ...handlers, wheelZoom } + ); + + drawLines(containerRef.current, lineData, enabledLine, getScales(), activeUnit, colors, singlePlot, evt.Context.EventInfo?.EventId ?? null); + drawMarkers(containerRef.current, lineData, getScales(), colors); + updateLimits(); + updateDurationWindowRect(containerRef.current, xScaleRef.current, evt.Context.EventInfo?.Inception ?? 0, evt.Context.EventInfo?.DurationEndTime ?? 0, plotMarkers); + updateLineVisibility(containerRef.current, lineData, enabledLine); + updateMarkerVisibility(containerRef.current, lineData, enabledLine); + updateYAxisVisibility(containerRef.current, relevantUnits, enabledUnits); + setCreated(true); + }, [lineData, loading, firstAnalyticOption]); + + // Resize: reposition axes, recompute scale ranges + React.useEffect(() => { + if (xScaleRef.current == null || yScaleRef.current == null) return; + + updateXAxisPositionOnResize(containerRef.current, props.height, props.width); + updateYAxisPositionsOnResize(containerRef.current, relevantUnits, getScales().y, props.height, props.width); + + xScaleRef.current.domain([startTime, endTime]).range(xRange); + + const container = d3.select(containerRef.current); + container.select('.clip').attr('width', props.width - 110).attr('height', props.height - 60); + container.select('.fftwindow').attr('height', props.height - 60); + container.select('.Overlay').attr('width', props.width - 110); + updateLimits(); + + }, [props.height, props.width]); + + // Visibility: legend enable/disable + React.useEffect(() => { + if (lineData == null || lineData.length === 0) return; + updateLineVisibility(containerRef.current, lineData, enabledLine); + updateMarkerVisibility(containerRef.current, lineData, enabledLine); + updateYAxisVisibility(containerRef.current, relevantUnits, enabledUnits); + }, [enabledLine]); + + // Scale/limits update: y domain, x range, then re-render geometry + React.useEffect(() => { + if (yScaleRef.current == null || xScaleRef.current == null) return; + + relevantUnits.forEach(unit => { + if ((yScaleRef.current as any)[unit] && yLimits?.[unit]) + (yScaleRef.current as any)[unit].domain(yLimits[unit]); + }); + + xScaleRef.current.domain([startTime, endTime]).range(xRange); + + if (yLimits) updateLimits(); + + }, [activeUnit, yLimits, startTime, endTime, isZoomed, timeUnit, lineData, useRelevantTime]); + + // Colors: update line and marker colors on theme change + React.useEffect(() => { + updateLineColors(containerRef.current, colors); + updateMarkerColors(containerRef.current, colors); + }, [colors]); + + // FFT window D3: render the FFT window overlay + React.useEffect(() => { + updateFFTWindowD3(containerRef.current, props.dataKey.DataType, currentFFTWindow, showFFT, mouseMode); + }, [fftWindow, showFFT, currentFFTWindow, mouseMode]); + + // Duration window: update rect and marker positions + React.useEffect(() => { + if (xScaleRef.current == null || evt.Context.EventInfo == null) return; + updateDurationWindowRect(containerRef.current, xScaleRef.current, evt.Context.EventInfo.Inception, evt.Context.EventInfo.DurationEndTime, plotMarkers); + }, [plotMarkers, startTime, endTime, props.width, props.height, timeUnit]); + + // dataKey / options change: clear and recreate the whole plot + React.useEffect(() => { + d3.select(containerRef.current).select('svg.root').select('g.root').remove(); + + if (loading === 'Loading' || lineData?.length === 0) { + setCreated(false); + return; + } + + CreateLinePlot( + containerRef.current, + xScaleRef, + yScaleRef as { current: Record> }, + { + height: props.height, + width: props.width, + dataKey: props.dataKey, + yLimits, + enabledUnits, + yLabels, + startTime, + endTime, + showFFT, + plotMarkers, + fftWindow, + currentFFTWindow, + mouseMode, + timeCtx: buildTimeCtx(), + displayLabel: GetDisplayLabel(props.dataKey.DataType, firstAnalyticOption), + eventInfo: evt.Context.EventInfo, + }, + { ...handlers, wheelZoom } + ); + + drawLines(containerRef.current, lineData, enabledLine, getScales(), activeUnit, colors, singlePlot, evt.Context.EventInfo?.EventId ?? null); + drawMarkers(containerRef.current, lineData, getScales(), colors); + updateLimits(); + updateLineVisibility(containerRef.current, lineData, enabledLine); + updateMarkerVisibility(containerRef.current, lineData, enabledLine); + updateYAxisVisibility(containerRef.current, relevantUnits, enabledUnits); + }, [props.dataKey, firstAnalyticOption]); + + // Y-axis labels: re-render when labels or font size change + React.useEffect(() => { + updateYAxisLabels(containerRef.current, relevantUnits, yLabels, yLblFontSize); + }, [yLabels, yLblFontSize]); + + return ( + <> + 0} + hasTrace={Object.values(enabledLine).some(v => v)} + zoomWindow={zoomWindow} + polyLines={[ + ...hoverLines, + ...(props.showToolTip && selectedPointLocation != null ? [{ className: 'selectedPoint', points: `${selectedPointLocation},20 ${selectedPointLocation},${lineBottom}`, style: { stroke: "#000", opacity: 1, strokeDasharray: "5,5" } }] : []), + ...(plotMarkers ? [ + { className: 'inception', points: `${inceptionLocation},20 ${inceptionLocation},${lineBottom}`, style: { stroke: "#a30000", strokeDasharray: "5,5", opacity: 0.5 } }, + { className: 'duration', points: `${durationLocation},20 ${durationLocation},${lineBottom}`, style: { stroke: "#a30000", strokeDasharray: "5,5", opacity: 0.5 } } + ] : []) + ]} + /> + {loading === 'Loading' || lineData?.length === 0 ? null : + + } + + ); +} + +export default LineChart; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/CreatePlot.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/CreatePlot.ts new file mode 100644 index 00000000..809af810 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/CreatePlot.ts @@ -0,0 +1,124 @@ +//****************************************************************************************************** +// CreatePlot.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; +import { OpenSee } from "../../../global"; +import { IFormatTimeContext } from "../../Utils/Types"; +import { CreatePlotSVG } from "../../Renderers/CreatePlotSVG"; +import { createYAxes } from "../../Renderers/YAxes"; +import { createXAxis } from "./XAxes"; + +export interface IPlotChromeParams { + height: number; + width: number; + dataKey: OpenSee.IGraphProps; + yLimits: Partial>; + enabledUnits: OpenSee.Unit[]; + yLabels: Partial>; + startTime: number; + endTime: number; + showFFT: boolean; + plotMarkers: boolean; + fftWindow: [number, number]; + currentFFTWindow: [number, number]; + mouseMode: OpenSee.MouseMode; + timeCtx: IFormatTimeContext; + displayLabel: string; + eventInfo: OpenSee.IEventInfo | null; +} + +export interface IPlotHandlers { + onMouseMove: (evt: any) => void; + onMouseOut: () => void; + onMouseDown: (evt: any) => void; + onMouseUp: () => void; + onMouseEnter: () => void; + onFFTMouseDown: (evt: any) => void; + onFFTMouseUp: () => void; + wheelZoom: d3.ZoomBehavior; +} + +export const CreateLinePlot = ( + containerEl: HTMLDivElement | null, + xScaleRef: { current: d3.ScaleLinear }, + yScaleRef: { current: Record> }, + params: IPlotChromeParams, + handlers: IPlotHandlers +) => { + if (containerEl == null) return; + + const { height, width, dataKey, yLimits, enabledUnits, yLabels, startTime, endTime, showFFT, plotMarkers, fftWindow, currentFFTWindow, mouseMode, timeCtx, displayLabel, eventInfo } = params; + + const svg = CreatePlotSVG( + containerEl, + yScaleRef, + { height, width, dataKey, yLimits, displayLabel }, + { ...handlers, wheelZoom: handlers.wheelZoom } + ); + + if (svg == null) return; + + xScaleRef.current = d3.scaleLinear().domain([startTime, endTime]).range([60, width - 110]); + + createXAxis(svg, xScaleRef.current, height, width, timeCtx); + createYAxes(svg, enabledUnits, yScaleRef.current, yLabels, height, width); + const clipId = `clipData-${dataKey.DataType}-${dataKey.EventId}`; + + // Duration window rect + if (eventInfo != null) + svg.append("rect").classed("DurationWindow", true) + .attr("clip-path", `url(#${clipId})`) + .attr("stroke", "#d3d3d3") + .attr("x", xScaleRef.current(eventInfo.Inception)) + .attr("width", xScaleRef.current(eventInfo.DurationEndTime) - xScaleRef.current(eventInfo.Inception)) + .style("opacity", plotMarkers ? 0.25 : 0) + .style("pointer-events", "none") + .attr("y", 20).attr("height", height - 60) + .attr("fill", "black"); + + // FFT window rect (only for Voltage/Current) + if (dataKey.DataType === "Voltage" || dataKey.DataType === "Current") + svg.append("rect").classed("fftWindow", true) + .attr("clip-path", `url(#${clipId})`) + .attr("stroke", "#000") + .style("z-index", 9999) + .attr("x", xScaleRef.current(fftWindow[0])) + .attr("width", currentFFTWindow[1] - currentFFTWindow[0]) + .style("opacity", showFFT ? 0.5 : 0) + .style("cursor", mouseMode === "fftMove" && showFFT ? "grab" : "default") + .style("pointer-events", mouseMode === "fftMove" && showFFT ? "auto" : "none") + .attr("y", 20).attr("height", height - 60) + .attr("fill", "black") + .on("mousemove", evt => { + evt.stopPropagation(); + handlers.onMouseMove(evt); + }) + .on("mousedown", evt => { + evt.stopPropagation(); + handlers.onFFTMouseDown(evt); + }) + .on("mouseup", evt => { + evt.stopPropagation(); + handlers.onFFTMouseUp(); + }); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/Lines.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/Lines.ts new file mode 100644 index 00000000..a849a06f --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/Lines.ts @@ -0,0 +1,173 @@ +//****************************************************************************************************** +// Lines.tsx - Gbtc +// +// Copyright © 2025, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2025 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; +import { seriesToKey } from "../../../Context/PlotKeys"; +import { OpenSee } from "../../../global"; + +export interface IScales { + x: d3.ScaleLinear; + y: Record>; +} + +export const createLineGen = ( + scales: IScales, + activeUnit: Partial> | null, + unit: OpenSee.Unit | null, + base: number | null +) => { + let factor = 1.0; + + if (unit && base && activeUnit?.[unit]) { + const f = activeUnit[unit]?.factor; + factor = f === undefined ? 1.0 / base : f; + } + + const xScale = scales.x; + const yScale = scales.y[unit ?? ""]; + + return d3.line<[number, number]>() + .x(d => xScale ? xScale(d[0]) : 0) + .y(d => yScale != null ? yScale(d[1] * factor) : 0) + .defined(d => { + if (xScale == null || yScale == null || !Number.isFinite(factor) || !Number.isFinite(d[0]) || !Number.isFinite(d[1])) + return false; + + return Number.isFinite(xScale(d[0])) && Number.isFinite(yScale(d[1] * factor)); + }); +} + +const timeBisector = d3.bisector((point: [number, number]) => point[0]); + +const getVisiblePoints = ( + dataPoints: Array<[number, number]>, + xScale: d3.ScaleLinear +): Array<[number, number]> => { + if (dataPoints.length < 3 || xScale == null) + return dataPoints; + + const domain = xScale.domain(); + const start = Math.min(domain[0], domain[1]); + const end = Math.max(domain[0], domain[1]); + + if (!Number.isFinite(start) || !Number.isFinite(end)) + return dataPoints; + + const firstTime = dataPoints[0][0]; + const lastTime = dataPoints[dataPoints.length - 1][0]; + + if (start <= firstTime && end >= lastTime) + return dataPoints; + + const from = Math.max(0, timeBisector.left(dataPoints, start) - 1); + const to = Math.min(dataPoints.length, timeBisector.right(dataPoints, end) + 1); + + if (to <= from) + return []; + + return dataPoints.slice(from, to); +} + +const getPathData = ( + d: OpenSee.iD3DataSeries, + scales: IScales, + activeUnit: Partial> | null, + base: number | null +): string | null => { + const useSmooth = d.SmoothDataPoints.length > 0; + const visiblePoints = getVisiblePoints(useSmooth ? d.SmoothDataPoints : d.DataPoints, scales.x); + const lineGen = createLineGen(scales, activeUnit, d.Unit, base); + + return useSmooth ? lineGen.curve(d3.curveNatural)(visiblePoints) : lineGen(visiblePoints); +} + +export const drawLines = ( + container: HTMLDivElement | null, + lineData: OpenSee.iD3DataSeries[], + enabledLine: Record, + scales: IScales, + activeUnit: Partial> | null, + colors: OpenSee.IColorCollection, + singlePlot: boolean, + currentEventId: number | null +) => { + if (container == null) return; + + const lines = d3.select(container) + .select(".DataContainer") + .selectAll(".Line") + .data(lineData); + + lines.enter() + .append("path") + .classed("Line", true) + .attr("type", d => `${d.Unit}`) + .attr("stroke", d => Object.keys(colors).includes(d.Color) ? colors[d.Color] : colors.random) + .attr("stroke-dasharray", d => singlePlot && currentEventId !== d.EventID ? 5 : 0) + .attr("d", d => { + if (enabledLine[seriesToKey(d)] !== true) + return null; + + return getPathData(d, scales, activeUnit, null); + }); + + lines.exit().remove(); +} + +export const updateLineGeometry = ( + container: HTMLDivElement | null, + enabledLine: Record, + scales: IScales, + activeUnit: Partial> | null +) => { + if (container == null) return; + + d3.select(container) + .select(".DataContainer") + .selectAll(".Line") + .attr("d", d => { + if (enabledLine[seriesToKey(d)] !== true) + return null; + + return getPathData(d, scales, activeUnit, d.BaseValue); + }); +} + +export const updateLineColors = (container: HTMLDivElement | null, colors: OpenSee.IColorCollection) => { + if (container == null) return; + + d3.select(container) + .select(".DataContainer") + .selectAll(".Line") + .attr("stroke", d => colors[d.Color as string] ?? colors.random); +} + +export const updateLineVisibility = (container: HTMLDivElement | null, lineData: OpenSee.iD3DataSeries[], enabledLine: Record) => { + if (container == null) return; + + d3.select(container) + .selectAll(".Line") + .data(lineData) + .classed("active", d => enabledLine[seriesToKey(d)] === true) + .attr("stroke-width", d => enabledLine[seriesToKey(d)] === true ? 2.5 : 0); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/Overlays.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/Overlays.ts new file mode 100644 index 00000000..731ec9e8 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/Overlays.ts @@ -0,0 +1,54 @@ +//****************************************************************************************************** +// Overlays.tsx - Gbtc +// +// Copyright © 2025, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2025 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; + +export const updateFFTWindow = ( + container: HTMLDivElement | null, + dataType: string, + currentFFTWindow: [number, number], + showFFT: boolean, + mouseMode: string +) => { + if (container == null || (dataType !== 'Voltage' && dataType !== 'Current')) + return; + + d3.select(container) + .select(".fftWindow") + .attr("x", currentFFTWindow[0]) + .attr("width", currentFFTWindow[1] - currentFFTWindow[0]) + .style("opacity", showFFT ? 0.5 : 0) + .style("cursor", mouseMode === "fftMove" && showFFT ? "grab" : "default") + .style("pointer-events", mouseMode === "fftMove" && showFFT ? "auto" : "none"); +} + +export const updateDurationWindowRect = (container: HTMLDivElement | null, xScale: d3.ScaleLinear, inception: number, durationEndTime: number, plotMarkers: boolean) => { + if (container == null) return; + + d3.select(container) + .select(".DurationWindow") + .attr("x", xScale(inception)) + .attr("width", xScale(durationEndTime) - xScale(inception)) + .style("opacity", plotMarkers ? 0.25 : 0) + .style("pointer-events", "none"); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/XAxes.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/XAxes.ts new file mode 100644 index 00000000..7965ce28 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/XAxes.ts @@ -0,0 +1,96 @@ +//****************************************************************************************************** +// XAxes.tsx - Gbtc +// +// Copyright © 2025, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2025 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + + +import * as d3 from "d3"; +import { OpenSee } from "../../../global"; +import { formatTimeTick } from "../../Utils/Utilities"; +import { IFormatTimeContext } from "../../Utils/Types"; + +export function createXAxis( + svg: d3.Selection, + xScale: d3.ScaleLinear, + height: number, + width: number, + timeCtx: IFormatTimeContext +): void { + svg.append("g").classed("xAxis", true) + .attr("transform", `translate(0,${height - 40})`) + .call(d3.axisBottom(xScale).tickFormat(d => formatTimeTick(d as number, timeCtx))); + + svg.append("text").classed("xAxisLabel", true) + .attr("transform", `translate(${(width - 210) / 2 + 60},${height - 5})`) + .style("text-anchor", "middle") + .text("Time"); +} + +export function updateXAxisTicks( + container: HTMLDivElement | null, + xScale: d3.ScaleLinear, + timeCtx: IFormatTimeContext +): void { + if (!container) return; + + d3.select(container).selectAll(".xAxis") + .transition() + .call(d3.axisBottom(xScale).tickFormat(d => formatTimeTick(d as number, timeCtx)) as any); +} + +export const updateXAxisLabel = ( + container: HTMLDivElement | null, + xScale: d3.ScaleLinear, + isOverlappingWaveform: boolean, + timeUnit: OpenSee.IUnitSetting +) => { + if (container == null) return; + + const h = xScale != null ? xScale.domain()[1] - xScale.domain()[0] : 100; + + let label: string; + if ((timeUnit as OpenSee.IUnitSetting).options?.[timeUnit.current]?.short !== "auto" && !isOverlappingWaveform) { + label = (timeUnit as OpenSee.IUnitSetting).options?.[timeUnit.current]?.short ?? ""; + } else if (isOverlappingWaveform) { + label = h < 100 ? "ms" : "s"; + } else if ((timeUnit as OpenSee.IUnitSetting).options?.[timeUnit.current]?.short === "ms since event") { + label = "ms"; + } else if ((timeUnit as OpenSee.IUnitSetting).options?.[timeUnit.current]?.short === "cycles") { + label = "cycle"; + } else { + label = h < 100 ? "ms" : "s"; + } + + d3.select(container) + .select(".xAxisLabel") + .text(`Time (${label})`); +} + +export const updateXAxisPositionOnResize = (container: HTMLDivElement | null, height: number, width: number) => { + if (container == null) return; + + d3.select(container).select(".xAxis").attr("transform", `translate(0,${height - 40})`); + d3.select(container).select(".xAxisLabel") + .attr("transform", `translate(${(width - 210) / 2 + 60},${height - 5})`); + d3.select(container).select(".plotTitle") + .attr("transform", `translate(${(width - 210) / 2 + 60},20)`) + .style("font-weight", "bold"); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/hooks/useFFTWindow.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/hooks/useFFTWindow.ts new file mode 100644 index 00000000..76cb81a7 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/hooks/useFFTWindow.ts @@ -0,0 +1,24 @@ +import * as React from 'react'; +import * as d3 from 'd3'; + +export interface IFFTWindowState { + currentFFTWindow: [number, number]; + setCurrentFFTWindow: React.Dispatch>; + oldFFTWindow: [number, number]; + setOldFFTWindow: React.Dispatch>; +} + +export function useFFTWindow( + xScaleRef: React.MutableRefObject>, + fftWindow: [number, number] +): IFFTWindowState { + const [currentFFTWindow, setCurrentFFTWindow] = React.useState<[number, number]>(fftWindow); + const [oldFFTWindow, setOldFFTWindow] = React.useState<[number, number]>([0, 0]); + + React.useEffect(() => { + if (xScaleRef.current) + setCurrentFFTWindow([xScaleRef.current(fftWindow[0]), xScaleRef.current(fftWindow[1])]); + }, [fftWindow]); + + return { currentFFTWindow, setCurrentFFTWindow, oldFFTWindow, setOldFFTWindow }; +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/hooks/useMouseInteractions.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/hooks/useMouseInteractions.ts new file mode 100644 index 00000000..4a50124f --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/hooks/useMouseInteractions.ts @@ -0,0 +1,339 @@ +import * as d3 from 'd3'; +import * as React from 'react'; +import { PlotStateActionContext, PlotDataMap } from '../../../Context/PlotStateContext'; +import { OpenSee } from '../../../global'; + +export interface IMouseInteractionInputs { + containerRef: React.MutableRefObject; + xScaleRef: React.MutableRefObject>; + yScaleRef: React.MutableRefObject> | {}>; + primaryAxis: string; + hover: [number, number]; + setHover: (h: [number, number]) => void; + isOverlappingWaveform: boolean; + mouseMode: OpenSee.MouseMode; + zoomMode: OpenSee.ZoomMode; + setAnalytic: React.Dispatch>; + plotData: PlotDataMap; + dataKey: OpenSee.IGraphProps; + width: number; + height: number; + fftWindow: [number, number]; + startTime: number; + endTime: number; + yLimits: Partial>; + oldFFTWindow: [number, number]; + setOldFFTWindow: React.Dispatch>; + setCurrentFFTWindow: React.Dispatch>; +} + +export interface IMouseInteractionResult { + handlers: { + onMouseMove: (evt: any) => void; + onMouseDown: (evt: any) => void; + onMouseUp: () => void; + onMouseOut: () => void; + onMouseEnter: () => void; + onFFTMouseDown: (evt: any) => void; + onFFTMouseUp: () => void; + }; + wheelZoom: d3.ZoomBehavior; + mouseDown: boolean; + fftMouseDown: boolean; + pointMouse: [number, number]; +} + +interface IPanOrigin { + xLimits: [number, number]; + yLimits: Partial>; + xScale: d3.ScaleLinear; + yScale: d3.ScaleLinear | null; + mouse: [number, number]; +} + +export const useMouseInteractions = (params: IMouseInteractionInputs): IMouseInteractionResult => { + const { + containerRef, xScaleRef, yScaleRef, primaryAxis, + hover, setHover, isOverlappingWaveform, + mouseMode, zoomMode, setAnalytic, + plotData, dataKey, + width, height, fftWindow, + startTime, endTime, yLimits, + oldFFTWindow, setOldFFTWindow, setCurrentFFTWindow, + } = params; + + const stateActions = React.useContext(PlotStateActionContext); + + const [mouseDown, setMouseDown] = React.useState(false); + const [fftMouseDown, setFFTMouseDown] = React.useState(false); + const [mouseDownInit, setMouseDownInit] = React.useState(false); + const [pointMouse, setPointMouse] = React.useState<[number, number]>([0, 0]); + const [leftSelectCounter, setLeftSelectCounter] = React.useState(0); + const panOriginRef = React.useRef(null); + const fftMouseDownRef = React.useRef(false); + + // rAF coalescing for hover updates: many mousemove events per frame collapse to a single setHover at most once per animation frame. + const pendingHoverRef = React.useRef<[number, number] | null>(null); + const hoverRafRef = React.useRef(null); + + React.useEffect(() => { + return () => { + if (hoverRafRef.current != null) { + cancelAnimationFrame(hoverRafRef.current); + hoverRafRef.current = null; + } + }; + }, []); + + React.useEffect(() => { + if (!fftMouseDown) return; + + const handleMouseUp = () => { MouseUp(); }; + window.addEventListener('mouseup', handleMouseUp); + return () => { window.removeEventListener('mouseup', handleMouseUp); }; + }, [fftMouseDown]); + + React.useEffect(() => { + if (leftSelectCounter === 0 || leftSelectCounter === 1) return; + const handle = setTimeout(() => { MouseLeft(); }, 500); + return () => { clearTimeout(handle); }; + }, [leftSelectCounter]); + + // Commit zoom/FFT action on mouse release + React.useEffect(() => { + if (!mouseDownInit) { + setMouseDownInit(true); + return; + } + + if (!mouseDown && mouseMode === 'zoom' && zoomMode === 'x') { + if (isOverlappingWaveform) + stateActions.SetCycleLimit(Math.min(pointMouse[0], hover[0]), Math.max(pointMouse[0], hover[0]), plotData); + else + stateActions.SetTimeLimit(Math.min(pointMouse[0], hover[0]), Math.max(pointMouse[0], hover[0]), plotData); + } + else if (!mouseDown && mouseMode === 'zoom' && zoomMode === 'y') + stateActions.SetZoomedLimits([Math.min(pointMouse[1], hover[1]), Math.max(pointMouse[1], hover[1])], dataKey, plotData); + else if (!mouseDown && mouseMode === 'zoom' && zoomMode === 'xy') { + if (isOverlappingWaveform) + stateActions.SetCycleLimit(Math.min(pointMouse[0], hover[0]), Math.max(pointMouse[0], hover[0]), plotData); + else + stateActions.SetTimeLimit(Math.min(pointMouse[0], hover[0]), Math.max(pointMouse[0], hover[0]), plotData); + stateActions.SetZoomedLimits([Math.min(pointMouse[1], hover[1]), Math.max(pointMouse[1], hover[1])], dataKey, plotData); + } + else if (!fftMouseDown && mouseMode === 'fftMove' && pointMouse[0] < oldFFTWindow[1] && pointMouse[0] > oldFFTWindow[0]) { + const deltaT = pointMouse[0] - oldFFTWindow[0]; + const deltaData = oldFFTWindow[1] - oldFFTWindow[0]; + let Tstart = hover[0] - deltaT; + Tstart = (Tstart < xScaleRef.current.domain()[0] ? xScaleRef.current.domain()[0] : Tstart); + Tstart = ((Tstart + deltaData) > xScaleRef.current.domain()[1] ? xScaleRef.current.domain()[1] - deltaData : Tstart); + setAnalytic(a => ({ ...a, FFTStartTime: Tstart })); + } + }, [mouseDown, fftMouseDown]); + + // FFT-drag: update state while mouse moves + React.useEffect(() => { + if (mouseMode === 'fftMove' && fftMouseDown && pointMouse[0] < oldFFTWindow[1] && pointMouse[0] > oldFFTWindow[0]) + setCurrentFFTWindow([ + xScaleRef.current(oldFFTWindow[0] + hover[0] - pointMouse[0]), + xScaleRef.current(oldFFTWindow[1] + hover[0] - pointMouse[0]), + ]); + }, [hover]); + + const MouseMove = (evt: any) => { + const [x0, y0] = getClampedPointer(evt, width, height); + + pendingHoverRef.current = [x0, y0]; + if (hoverRafRef.current == null) { + hoverRafRef.current = requestAnimationFrame(() => { + hoverRafRef.current = null; + if (pendingHoverRef.current != null) { + applyPan(pendingHoverRef.current); + setHover(getDataPoint(pendingHoverRef.current)); + pendingHoverRef.current = null; + } + }); + } + } + + const MouseDown = (evt: any) => { + const rawPointer = d3.pointer(evt, evt.currentTarget); + const [x0, y0] = clampPointer(rawPointer[0], rawPointer[1], width, height); + + const t0 = xScaleRef.current.invert(x0); + const d0 = (yScaleRef.current as any)[primaryAxis].invert(y0); + + setMouseDown(true); + setPointMouse([t0, d0]); + + if (mouseMode === 'fftMove') { + setOldFFTWindow(() => fftWindow); + const isFFTWindowHit = t0 < fftWindow[1] && t0 > fftWindow[0]; + fftMouseDownRef.current = isFFTWindowHit; + setFFTMouseDown(isFFTWindowHit); + return; + } + + if (mouseMode === 'pan') { + const yScale = (yScaleRef.current as any)[primaryAxis] as d3.ScaleLinear | undefined; + panOriginRef.current = { + xLimits: [startTime, endTime], + yLimits: copyYLimits(yLimits), + xScale: xScaleRef.current.copy(), + yScale: yScale?.copy() ?? null, + mouse: [x0, y0], + }; + } + else + panOriginRef.current = null; + + if (isOverlappingWaveform) return; + + if (rawPointer[0] > 60 && rawPointer[0] < width - 110 && mouseMode === 'select') + stateActions.SetSelectPoint(t0, plotData); + + setOldFFTWindow(() => fftWindow); + } + + const FFTMouseDown = (evt: any) => { + fftMouseDownRef.current = true; + setFFTMouseDown(true); + const pointer = d3.pointer(evt, evt.currentTarget); + const x0 = pointer[0]; + const y0 = pointer[1]; + + const t0 = xScaleRef.current.invert(x0); + const d0 = (yScaleRef.current as any)[primaryAxis].invert(y0); + + setPointMouse([t0, d0]); + setOldFFTWindow(() => fftWindow); + } + + const flushPendingHover = () => { + if (hoverRafRef.current != null) { + cancelAnimationFrame(hoverRafRef.current); + hoverRafRef.current = null; + } + if (pendingHoverRef.current != null) { + applyPan(pendingHoverRef.current); + setHover(getDataPoint(pendingHoverRef.current)); + pendingHoverRef.current = null; + } + } + + const MouseUp = () => { + flushPendingHover(); + setMouseDown(false); + fftMouseDownRef.current = false; + setFFTMouseDown(false); + panOriginRef.current = null; + } + + const MouseOut = () => { + if (fftMouseDownRef.current) return; + setLeftSelectCounter(() => -1); + } + + const MouseLeft = () => { + if (fftMouseDownRef.current) return; + setMouseDown(false); + panOriginRef.current = null; + } + + const applyPan = (pointer: [number, number]) => { + const panOrigin = panOriginRef.current; + if (mouseMode !== 'pan' || panOrigin == null) return; + + const deltaT = panOrigin.xScale.invert(pointer[0]) - panOrigin.xScale.invert(panOrigin.mouse[0]); + const deltaData = panOrigin.yScale == null ? 0 : panOrigin.yScale.invert(pointer[1]) - panOrigin.yScale.invert(panOrigin.mouse[1]); + + if (zoomMode === 'x' || zoomMode === 'xy') { + if (!isOverlappingWaveform) + stateActions.SetTimeLimit(panOrigin.xLimits[0] - deltaT, panOrigin.xLimits[1] - deltaT, plotData); + else + stateActions.SetCycleLimit(panOrigin.xLimits[0] - deltaT, panOrigin.xLimits[1] - deltaT, plotData); + } + + const initialYLimits = (panOrigin.yLimits as any)[primaryAxis]; + if (initialYLimits != null && (zoomMode === 'y' || zoomMode === 'xy')) + stateActions.SetZoomedLimits( + [initialYLimits[0] - deltaData, initialYLimits[1] - deltaData], + dataKey, plotData + ); + } + + const getDataPoint = (pointer: [number, number]): [number, number] => { + return [ + xScaleRef.current.invert(pointer[0]), + (yScaleRef.current as any)[primaryAxis].invert(pointer[1]), + ]; + } + + const wheelZoom = d3.zoom() + .filter(event => event.type === 'wheel') + .on('zoom', (event) => { + const newTime = event.transform.rescaleX(xScaleRef.current).domain(); + const newYLimits = event.transform.rescaleY((yScaleRef.current as any)[primaryAxis]).domain(); + + if (mouseMode === 'zoom' && zoomMode === 'x') { + if (isOverlappingWaveform) + stateActions.SetCycleLimit(newTime[0], newTime[1], plotData); + else + stateActions.SetTimeLimit(newTime[0], newTime[1], plotData); + } + + if (mouseMode === 'zoom' && zoomMode === 'y') + stateActions.SetZoomedLimits(newYLimits, dataKey, plotData); + + if (mouseMode === 'zoom' && zoomMode === 'xy') { + if (isOverlappingWaveform) + stateActions.SetCycleLimit(newTime[0], newTime[1], plotData); + else + stateActions.SetTimeLimit(newTime[0], newTime[1], plotData); + stateActions.SetZoomedLimits(newYLimits, dataKey, plotData); + } + }); + + return { + handlers: { + onMouseMove: MouseMove, + onMouseDown: MouseDown, + onMouseUp: MouseUp, + onMouseOut: MouseOut, + onMouseEnter: () => setLeftSelectCounter(1), + onFFTMouseDown: FFTMouseDown, + onFFTMouseUp: MouseUp, + }, + wheelZoom, + mouseDown, + fftMouseDown, + pointMouse, + }; +} + +const getClampedPointer = (evt: any, width: number, height: number): [number, number] => { + const pointer = d3.pointer(evt, evt.currentTarget); + return clampPointer(pointer[0], pointer[1], width, height); +} + +const clampPointer = (x: number, y: number, width: number, height: number): [number, number] => { + let x0 = x; + let y0 = y; + + if (x0 < 60) x0 = 60; + if (x0 > (width - 110)) x0 = width - 110; + if (y0 < 20) y0 = 20; + if (y0 > (height - 40)) y0 = height - 40; + + return [x0, y0]; +} + +const copyYLimits = (currentYLimits: Partial>): Partial> => { + const copied = {} as Partial>; + Object.keys(currentYLimits).forEach(unit => { + const limits = (currentYLimits as any)[unit]; + if (limits != null) + (copied as any)[unit] = [limits[0], limits[1]]; + }); + return copied; +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/hooks/useTimeFormatContext.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/hooks/useTimeFormatContext.ts new file mode 100644 index 00000000..f9cf64e3 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/hooks/useTimeFormatContext.ts @@ -0,0 +1,35 @@ +import * as React from 'react'; +import * as d3 from 'd3'; +import { OpenSee } from '../../../global'; +import { IFormatTimeContext } from '../../Utils/Types'; + +export function useTimeFormatContext( + xScaleRef: React.MutableRefObject>, + isOverlappingWaveform: boolean, + overlappingWaveTimeUnit: number, + timeUnit: OpenSee.IUnitSetting, + originalStartTime: number, + useRelevantTime: boolean, + isOriginalEvt: boolean, + overlappingEvents: OpenSee.OverlappingEvents[], + dataKeyEventId: number, + inceptionTime: number, + startTime: number +): () => IFormatTimeContext { + return React.useCallback((): IFormatTimeContext => ({ + xDomainWidth: xScaleRef.current != null + ? xScaleRef.current.domain()[1] - xScaleRef.current.domain()[0] + : 100, + isOverlappingWaveform, + overlappingWaveTimeUnit, + timeUnit, + originalStartTime, + useRelevantTime, + isOriginalEvt, + overlappingEvents, + dataKeyEventId, + inceptionTime, + startTime, + }), [isOverlappingWaveform, overlappingWaveTimeUnit, timeUnit, originalStartTime, + useRelevantTime, isOriginalEvt, overlappingEvents, dataKeyEventId, inceptionTime, startTime]); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/PolyLine.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/PolyLine.tsx new file mode 100644 index 00000000..7d077d91 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/PolyLine.tsx @@ -0,0 +1,40 @@ +//****************************************************************************************************** +// PolyLine.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as React from 'react'; + +export interface PolyLineSpec { + className: string; + points: string; + style: React.CSSProperties; +} + +const PolyLine = (props: PolyLineSpec) => { + return ( + + + + ); +} + +export default PolyLine; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/CreatePlotSVG.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/CreatePlotSVG.ts new file mode 100644 index 00000000..8d3c8da3 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/CreatePlotSVG.ts @@ -0,0 +1,125 @@ +//****************************************************************************************************** +// ChromeBuilder.ts - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; +import { OpenSee } from "../../global"; + +export interface IChromeBuilderParams { + height: number; + width: number; + dataKey: OpenSee.IGraphProps; + yLimits: Partial>; + displayLabel: string; +} + +export interface IChromeHandlers { + onMouseMove: (evt: any) => void; + onMouseOut: () => void; + onMouseDown: (evt: any) => void; + onMouseUp: () => void; + onMouseEnter?: () => void; + wheelZoom?: d3.ZoomBehavior; +} + +export const CreatePlotSVG = ( + containerEl: HTMLDivElement | null, + yScaleRef: { current: Record> }, + params: IChromeBuilderParams, + handlers: IChromeHandlers +) => { + if (containerEl == null) return null; + + const { height, width, dataKey, yLimits, displayLabel } = params; + d3.select(containerEl).select("svg.root").select("g.root").remove(); + + const svg = d3.select(containerEl).select("svg.root") + .append("g").classed("root", true) + .attr("transform", "translate(10,0)"); + + if (yLimits) { + Object.keys(yLimits).forEach(unit => { + if ((yLimits as any)[unit]) + yScaleRef.current[unit] = d3.scaleLinear().domain((yLimits as any)[unit]).range([height - 40, 20]); + else + yScaleRef.current[unit] = d3.scaleLinear().domain([0, 1]).range([height - 40, 20]); + }); + } + + svg.append("text").classed("plotTitle", true) + .attr("transform", `translate(${(width - 210) / 2 + 60},20)`) + .style("text-anchor", "middle") + .style("font-weight", "bold") + .text(displayLabel); + + const clipId = `clipData-${dataKey.DataType}-${dataKey.EventId}`; + svg.append("defs").append("svg:clipPath") + .attr("id", clipId) + .append("svg:rect").classed("clip", true) + .attr("width", width - 170) + .attr("height", height - 60) + .attr("x", 60) + .attr("y", 20); + + svg.append("g").classed("DataContainer", true) + .attr("clip-path", `url(#${clipId})`) + .style("transition", "d 0.5s") + .attr("fill", "none") + .attr("stroke-width", 0.0); + + const overlay = svg.append("svg:rect").classed("Overlay", true) + .attr("width", width - 110) + .attr("height", "100%") + .attr("x", 20) + .attr("y", 0) + .style("opacity", 0) + .on("mousemove", evt => { + evt.stopPropagation(); + handlers.onMouseMove(evt); + }) + .on("mouseout", evt => { + evt.stopPropagation(); + handlers.onMouseOut(); + }) + .on("mousedown", evt => { + evt.stopPropagation(); + handlers.onMouseDown(evt); + }) + .on("mouseup", evt => { + evt.stopPropagation(); + handlers.onMouseUp(); + }); + + if (handlers.onMouseEnter != null) + overlay.on("mouseenter", evt => { + evt.stopPropagation(); + handlers.onMouseEnter?.(); + }); + + if (handlers.wheelZoom != null) + overlay.call(handlers.wheelZoom).on("wheel.overlayBlock", evt => { + evt.preventDefault(); + evt.stopPropagation(); + }); + + return svg; +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/Markers.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/Markers.ts new file mode 100644 index 00000000..4d72311d --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/Markers.ts @@ -0,0 +1,104 @@ +//****************************************************************************************************** +// Markers.ts - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; +import { seriesToKey } from "../../Context/PlotKeys"; +import { OpenSee } from "../../global"; + +export interface ILinearScales { + x: d3.ScaleLinear; + y: Record>; +} + +interface IMarker { + x: number; + y: number; + unit: string; + base: number; +} + +export const drawMarkers = ( + container: HTMLDivElement | null, + lineData: OpenSee.iD3DataSeries[], + scales: ILinearScales, + colors: OpenSee.IColorCollection +) => { + if (container == null) return; + + const points = d3.select(container).select(".DataContainer") + .selectAll(".Markers") + .data(lineData) + .enter() + .append("g") + .attr("fill", d => Object.keys(colors).includes(d.Color) ? colors[d.Color] : colors.random) + .classed("Markers", true) + .selectAll("circle") + .data(d => d.DataMarker.map(v => ({ + x: v[0], y: v[1], unit: d.Unit as string, base: d.BaseValue + } as IMarker))); + + points.enter() + .append("circle") + .classed("Circle", true) + .attr("cx", (d: IMarker) => isNaN(scales.x((d as any)[0])) ? null : scales.x(d.x)) + .attr("cy", (d: IMarker) => isNaN(scales.y[d.unit]((d as any)[1])) ? null : scales.y[d.unit](d.y)) + .attr("r", 10); + + points.exit().remove(); +} + +export const updateMarkerGeometry = ( + container: HTMLDivElement | null, + scales: ILinearScales, + activeUnit: Partial> | null +) => { + if (container == null) return; + + d3.select(container) + .select(".DataContainer") + .selectAll("circle") + .attr("cx", (d: IMarker) => isNaN(scales.x(d.x)) ? null : scales.x(d.x)) + .attr("cy", (d: IMarker) => { + let factor = 1.0; + if (activeUnit?.[d.unit] != undefined) + factor = activeUnit[d.unit].factor === undefined ? (1.0 / d.base) : factor; + return isNaN(scales.y[d.unit](d.y)) ? null : scales.y[d.unit](d.y * factor); + }); +} + +export const updateMarkerColors = (container: HTMLDivElement | null, colors: OpenSee.IColorCollection) => { + if (container == null) return; + + d3.select(container).select(".DataContainer") + .selectAll(".Markers") + .attr("fill", d => colors[d.Color as string] ?? colors.random); +} + +export const updateMarkerVisibility = (container: HTMLDivElement | null, lineData: OpenSee.iD3DataSeries[], enabledLine: Record) => { + if (container == null) return; + + d3.select(container).selectAll(".Markers") + .data(lineData) + .classed("active", d => enabledLine[seriesToKey(d)] === true) + .attr("opacity", d => enabledLine[seriesToKey(d)] === true ? 1.0 : 0); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/YAxes.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/YAxes.ts new file mode 100644 index 00000000..318c6ed3 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/YAxes.ts @@ -0,0 +1,196 @@ +//****************************************************************************************************** +// YAxes.ts - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; +import { OpenSee } from "../../global"; +import { formatValueTick } from "../Utils/Utilities"; + +export const createYAxes = ( + svg: d3.Selection, + enabledUnits: OpenSee.Unit[], + yScaleCollection: Record>, + yLabels: Partial>, + height: number, + width: number +) => { + let isAxisLeft = true; + + enabledUnits.forEach(unit => { + const axisTransform = isAxisLeft ? "translate(60,0)" : `translate(${width - 110},0)`; + + svg.append("g") + .classed("yAxis", true) + .attr("type", `${unit}`) + .attr("transform", axisTransform) + .call( + isAxisLeft + ? d3.axisLeft(yScaleCollection[unit]).tickFormat(d => formatValueTick(d as number, unit, yScaleCollection)) + : d3.axisRight(yScaleCollection[unit]).tickFormat(d => formatValueTick(d as number, unit, yScaleCollection)) + ) + .style("opacity", 1); + + const labelYPos = isAxisLeft ? 2 : width - 70; + + svg.append("text") + .classed(isAxisLeft ? "yAxisLabelLeft" : "yAxisLabelRight", true) + .attr("type", `${unit}`) + .attr("x", -(height / 2 - 20)) + .attr("y", labelYPos) + .attr("dy", "1em") + .attr("transform", "rotate(-90)") + .style("text-anchor", "middle") + .style("opacity", 1) + .text(yLabels[unit] ?? ""); + + isAxisLeft = !isAxisLeft; + }); +} + +export const updateYAxes = ( + container: HTMLDivElement | null, + enabledUnits: OpenSee.Unit[], + yScaleCollection: Record>, + width: number, + dataType: string, + eventId: number +) => { + if (container == null) return; + + const sel = d3.select(container); + let isAxisLeft = true; + let currentAxis = 0; + + enabledUnits?.forEach(unit => { + const axisType = `[type='${unit}']`; + const firstLeftAxisType = `[type='${enabledUnits[0]}']`; + const firstRightAxisType = `[type='${enabledUnits[1]}']`; + const yScale = yScaleCollection[unit]; + if (yScale == null) return; + + if (isAxisLeft) { + if (currentAxis > 1) { + sel.selectAll(`.yAxis${firstLeftAxisType}`).attr("transform", "translate(120, 0)"); + sel.selectAll(`.yAxisLabelLeft${firstLeftAxisType}`).attr("y", "62"); + } + sel.selectAll(`.yAxis${axisType}`) + .transition() + .call(d3.axisLeft(yScale).tickFormat(d => formatValueTick(d as number, unit, yScaleCollection)) as any); + } else { + if (currentAxis > 2) { + sel.selectAll(`.yAxis${firstRightAxisType}`).attr("transform", `translate(${width - 170},0)`); + sel.selectAll(`.yAxisLabelRight${firstRightAxisType}`).attr("y", width - 135); + } + sel.selectAll(".yAxis") + .selectAll(`[type='${unit}']`) + .transition() + .call(d3.axisRight(yScale).tickFormat(d => formatValueTick(d as number, unit, yScaleCollection)) as any); + } + + isAxisLeft = !isAxisLeft; + currentAxis++; + }); + + const clipPath = sel.select(`#clipData-${dataType}-${eventId} > rect`); + const evtOverlay = sel.select("rect.Overlay"); + + if (enabledUnits.length < 3) { + clipPath.attr("x", 60).attr("width", width - 170); + evtOverlay.attr("x", 20).attr("width", width - 110); + return; + } + + if (enabledUnits.length === 3) { + clipPath.attr("x", 120).attr("width", width - 270); + evtOverlay.attr("x", 120).attr("width", width - 270); + } else if (enabledUnits.length === 4) { + clipPath.attr("x", 120).attr("width", width - 210 - 120); + evtOverlay.attr("x", 120).attr("width", width - 210 - 120); + } +} + +export const updateYAxisLabels = ( + container: HTMLDivElement | null, + relevantUnits: OpenSee.Unit[], + yLabels: Partial>, + fontSize: number +) => { + if (container == null) return; + + const sel = d3.select(container); + const fs = `${fontSize}rem`; + + relevantUnits.forEach(unit => { + sel.select(`.yAxisLabelLeft[type='${unit}']`).style("font-size", fs).text(yLabels[unit] ?? ""); + sel.select(`.yAxisLabelRight[type='${unit}']`).style("font-size", fs).text(yLabels[unit] ?? ""); + }); +} + +export const updateYAxisVisibility = ( + container: HTMLDivElement | null, + relevantUnits: OpenSee.Unit[], + enabledUnits: OpenSee.Unit[] +) => { + if (container == null) return; + + const sel = d3.select(container); + + relevantUnits.forEach(unit => { + const axisType = `[type='${unit}']`; + if (enabledUnits?.includes(unit)) { + sel.selectAll(`.yAxis${axisType}`).style("opacity", 1); + sel.selectAll(`.yAxisLabelLeft${axisType}`).style("opacity", 1); + sel.selectAll(`.yAxisLabelRight${axisType}`).style("opacity", 1); + } else { + sel.selectAll(`.yAxis${axisType}`).style("opacity", 0); + sel.selectAll(`.yAxisLabelLeft${axisType}`).style("opacity", 0); + sel.selectAll(`.yAxisLabelRight${axisType}`).style("opacity", 0); + } + }); +} + +export const updateYAxisPositionsOnResize = ( + container: HTMLDivElement | null, + relevantUnits: OpenSee.Unit[], + yScaleCollection: Record>, + height: number, + width: number +) => { + if (container == null) return; + + const sel = d3.select(container); + sel.select(".yAxisLabelLeft").attr("x", -(height / 2 - 20)); + sel.select(".yAxisLabelRight").attr("y", width - 120).attr("x", -(height / 2 - 20)); + + let isAxisLeft = true; + relevantUnits.forEach(unit => { + if (yScaleCollection[unit] == null) return; + + yScaleCollection[unit].range([height - 40, 20]); + + const axisType = `[type='${unit}']`; + const axisTransform = isAxisLeft ? "translate(60,0)" : `translate(${width - 110},0)`; + sel.selectAll(`.yAxis${axisType}`).attr("transform", axisTransform); + + isAxisLeft = !isAxisLeft; + }); +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/ZoomWindow.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/ZoomWindow.ts new file mode 100644 index 00000000..ca8c5cd1 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Renderers/ZoomWindow.ts @@ -0,0 +1,74 @@ +//****************************************************************************************************** +// ZoomWindow.ts - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code +// +//****************************************************************************************************** + +import * as d3 from "d3"; + +export interface ZoomWindowRect { + x: number; + y: number; + width: number; + height: number; +} + +// Computes the zoom-selection rectangle geometry, or null when no selection is in progress. +// Coordinates are in the plot's translated (translate(10,0)) coordinate system. +export const computeZoomWindow = ( + pxAtX: (d: number) => number, + primaryYScale: d3.ScaleLinear, + hover: [number, number], + pointMouse: [number, number], + mouseMode: string, + zoomMode: string, + mouseDown: boolean, + xDomainStart: number, + xDomainEnd: number, + height: number +): ZoomWindowRect | null => { + if (mouseMode !== "zoom" || !mouseDown || primaryYScale == null) return null; + + if (zoomMode === "x") + return { + x: pxAtX(Math.min(hover[0], pointMouse[0])), + width: Math.abs(pxAtX(hover[0]) - pxAtX(pointMouse[0])), + y: 20, + height: height - 60, + }; + + if (zoomMode === "y") + return { + x: pxAtX(xDomainStart), + width: pxAtX(xDomainEnd) - pxAtX(xDomainStart), + y: Math.min(primaryYScale(pointMouse[1]), primaryYScale(hover[1])), + height: Math.abs(primaryYScale(pointMouse[1]) - primaryYScale(hover[1])), + }; + + if (zoomMode === "xy") + return { + x: pxAtX(Math.min(hover[0], pointMouse[0])), + width: Math.abs(pxAtX(hover[0]) - pxAtX(pointMouse[0])), + y: Math.min(primaryYScale(pointMouse[1]), primaryYScale(hover[1])), + height: Math.abs(primaryYScale(pointMouse[1]) - primaryYScale(hover[1])), + }; + + return null; +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Utils/Types.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Utils/Types.ts new file mode 100644 index 00000000..0bfadd53 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Utils/Types.ts @@ -0,0 +1,81 @@ +//****************************************************************************************************** +// Types.ts - Gbtc +// +// Copyright 2021, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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. +// +//****************************************************************************************************** + +import React from 'react'; +import { OpenSee } from "../../global" +import { PlotDataMap } from '../../Context/PlotStateContext'; +import * as d3 from 'd3'; + +export interface IFormatTimeContext { + xDomainWidth: number; + isOverlappingWaveform: boolean; + overlappingWaveTimeUnit: number; + timeUnit: OpenSee.IUnitSetting; + originalStartTime: number; + useRelevantTime: boolean; + isOriginalEvt: boolean; + overlappingEvents: OpenSee.OverlappingEvents[]; + dataKeyEventId: number; + inceptionTime: number; + startTime: number; +} + +export interface IChartScales> { + xScaleRef: React.MutableRefObject; + yScaleRef: React.MutableRefObject> | {}>; +} + +export interface ITooltipLocations { + toolTipLocation: number; + selectedPointLocation: number | null; + inceptionLocation: number; + durationLocation: number; +} + +export interface IPanZoomInteractionInputs { + containerRef: React.MutableRefObject; + yScaleRef: React.MutableRefObject> | {}>; + primaryAxis: string; + hover: [number, number]; + setHover: (h: [number, number]) => void; + mouseMode: OpenSee.MouseMode; + zoomMode: OpenSee.ZoomMode; + plotData: PlotDataMap; + dataKey: OpenSee.IGraphProps; + height: number; + width: number; + xDomainStart: number; + xDomainEnd: number; + yLimits: Partial>; + pxToDomainX: (px: number) => number; + pxAtDomainX: (d: number) => number; + setXLimits: (lo: number, hi: number, plotData: PlotDataMap) => void; + setYLimits: (limits: [number, number], key: OpenSee.IGraphProps, plotData: PlotDataMap) => void; +} + +export interface IPanZoomInteractionResult { + handlers: { + onMouseMove: (evt: any) => void; + onMouseDown: (evt: any) => void; + onMouseUp: () => void; + onMouseOut: () => void; + onMouseEnter: () => void; + }; + mouseDown: boolean; + pointMouse: [number, number]; +} \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Utils/Utilities.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Utils/Utilities.tsx new file mode 100644 index 00000000..6b7caa4d --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Utils/Utilities.tsx @@ -0,0 +1,392 @@ +//****************************************************************************************************** +// Utilities.tsx - Gbtc +// +// Copyright � 2021, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/23/2021 - C. Lackner +// Generated original version of source code +// +//****************************************************************************************************** + +import React from 'react'; +import { OpenSee } from "../../global" +import { defaultSettings } from "../../defaults" +import moment from "moment"; +import { GetTextWidth } from '@gpa-gemstone/helper-functions'; +import * as d3 from 'd3'; +import { IChartScales, IFormatTimeContext, IPanZoomInteractionInputs, IPanZoomInteractionResult, ITooltipLocations } from './Types'; + +export const GetDisplayLabel = (type: OpenSee.graphType, harmonic?: number): string => { + switch (type) { + case ('FirstDerivative'): + return "First Derivative" + case ('HighPassFilter'): + return "High Pass Filter" + case ('LowPassFilter'): + return "Low Pass Filter" + case ('ClippedWaveforms'): + return "Fixed Clipped Waveforms" + case ('OverlappingWave'): + return "Overlapping Waveform" + case ('MissingVoltage'): + return "Missing Voltage" + case ('Rectifier'): + return 'Rectifier Output'; + case ('RapidVoltage'): + return "Rapid Voltage Change" + case ('RemoveCurrent'): + return "Remove Current" + case ('Harmonic'): + return harmonic == null ? "Specified Harmonic" : `Specified Harmonic (${harmonic})` + case ('SymetricComp'): + return "Symmetrical Components" + case ('FaultDistance'): + return "Fault Distance" + case ('Restrike'): + return "Breaker Restrike" + case ('I2T'): + return "I2T" + default: + return (type as string) + } +}; + +export const formatValueTick = (d: number, unit: OpenSee.Unit, yScaleCollection: OpenSee.IUnitCollection> | {}): string => { + let h = 1; + + if (yScaleCollection) + h = yScaleCollection[unit].domain()[1] - yScaleCollection[unit].domain()[0] + + if (Math.abs(d) >= 100000) { + return d.toString().slice(0, 4) + '...'; + } + + if (h > 100) + return d.toFixed(0) + + if (h > 10) + return d.toFixed(1) + else + return d.toFixed(2) +}; + +export const formatTimeTick = (d: number, ctx: IFormatTimeContext): string => { + const TS = moment(d); + let h = ctx.xDomainWidth; + + if (ctx.isOverlappingWaveform) { + if (defaultSettings.OverlappingWaveTimeUnit.options?.[ctx.overlappingWaveTimeUnit]?.short === "ms") { + if (h < 2) + return d.toFixed(3) + if (h < 5) + return d.toFixed(2) + else + return d.toFixed(1) + } else if (defaultSettings.OverlappingWaveTimeUnit.options?.[ctx.overlappingWaveTimeUnit]?.short === "cycles") { + const cyc = d * 60.0 / 1000.0; + h = h * 60.0 / 1000.0; + if (h < 2) + return cyc.toFixed(3) + if (h < 5) + return cyc.toFixed(2) + else + return cyc.toFixed(1) + } + + } + else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'auto') { + if (h < 100) + return TS.format("SSS.S") + else if (h < 1000) + return TS.format("ss.SS") + else + return TS.format("ss.S") + } + else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 's') { + if (h < 100) + return TS.format("ss.SSS") + else if (h < 1000) + return TS.format("ss.SS") + else + return TS.format("ss.S") + } + else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'ms') + if (h < 100) + return TS.format("SSS.S") + else + return TS.format("SSS") + + else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'min') + return TS.format("mm:ss") + + else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'ms since record') { + let ms = d - ctx.originalStartTime; + + if (ctx.useRelevantTime && !ctx.isOriginalEvt) { + const evt = ctx.overlappingEvents.find(evt => evt.EventID === ctx.dataKeyEventId); + if (evt != null) + ms = d - evt?.StartTime + } + + if (h < 2) + return ms.toFixed(3) + if (h < 5) + return ms.toFixed(2) + else + return ms.toFixed(1) + } + + else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'ms since inception') { + let ms = d - ctx.inceptionTime; + + if (ctx.useRelevantTime && !ctx.isOriginalEvt) { + const evt = ctx.overlappingEvents.find(evt => evt.EventID === ctx.dataKeyEventId); + if (evt != null) + ms = d - evt?.Inception + } + + if (h < 2) + return ms.toFixed(3) + if (h < 5) + return ms.toFixed(2) + else + return ms.toFixed(1) + } + + else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'cycles since record') { + const cyc = (d - ctx.startTime) * 60.0 / 1000.0; + + h = h * 60.0 / 1000.0; + if (h < 2) + return cyc.toFixed(3) + if (h < 5) + return cyc.toFixed(2) + else + return cyc.toFixed(1) + } + else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'cycles since inception') { + const cyc = (d - ctx.startTime) * 60.0 / 1000.0; + + h = h * 60.0 / 1000.0; + if (h < 2) + return cyc.toFixed(3) + if (h < 5) + return cyc.toFixed(2) + else + return cyc.toFixed(1) + } + + return d.toFixed(1); +}; + +export const useChartScales = ,>(initial: TX): IChartScales => { + const xScaleRef = React.useRef(initial); + const yScaleRef = React.useRef> | {}>({}); + return { xScaleRef, yScaleRef }; +}; + +export const useTooltipLocations = ( + xScaleRef: React.MutableRefObject>, + hover: [number, number], + points: OpenSee.IPoint[], + evtInfo: OpenSee.IEventInfo | null, + startTime: number, + endTime: number, + xRange: [number, number] +): ITooltipLocations => { + const [toolTipLocation, setTooltipLocation] = React.useState(10); + const [selectedPointLocation, setSelectedPointLocation] = React.useState(null); + const [inceptionLocation, setInceptionLocation] = React.useState(10); + const [durationLocation, setDurationLocation] = React.useState(10); + + React.useEffect(() => { + if (xScaleRef.current) + setTooltipLocation(xScaleRef.current(hover?.[0])); + }, [hover]); + + React.useEffect(() => { + if (!xScaleRef.current) return; + const newTime = points.length > 0 ? points[0].Time : null; + if (newTime != null && !isNaN(newTime)) + setSelectedPointLocation(xScaleRef.current(newTime)); + else + setSelectedPointLocation(null); + }, [points, startTime, endTime, xRange[0], xRange[1]]); + + React.useEffect(() => { + if (!xScaleRef.current || evtInfo == null) return; + setInceptionLocation(xScaleRef.current(evtInfo.Inception)); + setDurationLocation(xScaleRef.current(evtInfo.DurationEndTime)); + }, [startTime, endTime, evtInfo, xRange[0], xRange[1]]); + + return { toolTipLocation, selectedPointLocation, inceptionLocation, durationLocation }; +}; + +export const useYLabelFontSize = ( + yLabels: Partial>, + primaryAxis: string, + height: number +): number => { + const [fontSize, setFontSize] = React.useState(1); + + React.useEffect(() => { + let fs = 1; + let l = GetTextWidth('', '1rem', yLabels?.[primaryAxis]); + let r = GetTextWidth('', '1rem', yLabels?.[primaryAxis] ?? ""); + + while (((l > height - 60) || (r > height - 60)) && fs > 0.2) { + fs -= 0.05; + l = GetTextWidth('', `${fs}rem`, yLabels?.[primaryAxis]); + r = GetTextWidth('', `${fs}rem`, yLabels?.[primaryAxis] ?? ""); + } + if (fs !== fontSize) + setFontSize(fs); + }, [height, yLabels]); + + return fontSize; +}; + +export const usePanZoomInteractions = (inputs: IPanZoomInteractionInputs): IPanZoomInteractionResult => { + const { + containerRef, yScaleRef, primaryAxis, hover, setHover, + mouseMode, zoomMode, plotData, dataKey, + width, height, xDomainStart, xDomainEnd, yLimits, + pxToDomainX, setXLimits, setYLimits + } = inputs; + + const [mouseDown, setMouseDown] = React.useState(false); + const [mouseDownInit, setMouseDownInit] = React.useState(false); + const [pointMouse, setPointMouse] = React.useState<[number, number]>([0, 0]); + const [leftSelectCounter, setLeftSelectCounter] = React.useState(0); + const pendingHoverRef = React.useRef<[number, number] | null>(null); + const hoverRafRef = React.useRef(null); + + React.useEffect(() => { + return () => { + if (hoverRafRef.current != null) + cancelAnimationFrame(hoverRafRef.current); + }; + }, []); + + React.useEffect(() => { + if (leftSelectCounter === 0 || leftSelectCounter === 1) return; + const handle = setTimeout(() => { MouseLeft(); }, 500); + return () => { clearTimeout(handle); }; + }, [leftSelectCounter]); + + React.useEffect(() => { + if (!mouseDownInit) { + setMouseDownInit(true); + return; + } + + if (!mouseDown && mouseMode === 'zoom' && zoomMode === 'x') + setXLimits(Math.min(pointMouse[0], hover[0]), Math.max(pointMouse[0], hover[0]), plotData); + else if (!mouseDown && mouseMode === 'zoom' && zoomMode === 'y') + setYLimits([Math.min(pointMouse[1], hover[1]), Math.max(pointMouse[1], hover[1])], dataKey, plotData); + else if (!mouseDown && mouseMode === 'zoom' && zoomMode === 'xy') { + setXLimits(Math.min(pointMouse[0], hover[0]), Math.max(pointMouse[0], hover[0]), plotData); + setYLimits([Math.min(pointMouse[1], hover[1]), Math.max(pointMouse[1], hover[1])], dataKey, plotData); + } + }, [mouseDown]); + + React.useEffect(() => { + const deltaX = hover[0] - pointMouse[0]; + const deltaData = hover[1] - pointMouse[1]; + + if (mouseMode === 'pan' && mouseDown && (zoomMode === 'x' || zoomMode === 'xy') && Math.abs(deltaX) > 0) + setXLimits(xDomainStart - deltaX, xDomainEnd - deltaX, plotData); + + if (mouseMode === 'pan' && mouseDown && (zoomMode === 'y' || zoomMode === 'xy')) + setYLimits( + [(yLimits as any)[primaryAxis]?.[0] - deltaData, (yLimits as any)[primaryAxis]?.[1] - deltaData], + dataKey, + plotData + ); + }, [hover]); + + const queueHover = (value: [number, number]): void => { + pendingHoverRef.current = value; + if (hoverRafRef.current == null) { + hoverRafRef.current = requestAnimationFrame(() => { + hoverRafRef.current = null; + if (pendingHoverRef.current != null) { + setHover(pendingHoverRef.current); + pendingHoverRef.current = null; + } + }); + } + }; + + const getPointerDomain = (evt: any): [number, number] => { + let x0 = d3.pointer(evt, evt.currentTarget)[0]; + let y0 = d3.pointer(evt, evt.currentTarget)[1]; + + if (x0 < 60) x0 = 60; + if (x0 > (width - 140)) x0 = width - 140; + if (y0 < 20) y0 = 20; + if (y0 > (height - 40)) y0 = height - 40; + + const x = pxToDomainX(x0); + const y = (yScaleRef.current as any)[primaryAxis].invert(y0); + return [x, y]; + }; + + const MouseMove = (evt: any): void => { + queueHover(getPointerDomain(evt)); + }; + + const MouseDown = (evt: any): void => { + setMouseDown(true); + setPointMouse(getPointerDomain(evt)); + }; + + const flushPendingHover = (): void => { + if (hoverRafRef.current != null) { + cancelAnimationFrame(hoverRafRef.current); + hoverRafRef.current = null; + } + if (pendingHoverRef.current != null) { + setHover(pendingHoverRef.current); + pendingHoverRef.current = null; + } + }; + + const MouseUp = (): void => { + flushPendingHover(); + setMouseDown(false); + }; + + const MouseOut = (): void => { + setLeftSelectCounter(() => -1); + }; + + const MouseLeft = (): void => { + setMouseDown(false); + }; + + return { + handlers: { + onMouseMove: MouseMove, + onMouseDown: MouseDown, + onMouseUp: MouseUp, + onMouseOut: MouseOut, + onMouseEnter: () => setLeftSelectCounter(1), + }, + mouseDown, + pointMouse, + }; +}; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/About.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/About.tsx new file mode 100644 index 00000000..a1e8afb5 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/About.tsx @@ -0,0 +1,75 @@ +//****************************************************************************************************** +// About.tsx - Gbtc +// +// Copyright © 2019, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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/29/2019 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import { Modal } from '@gpa-gemstone/react-interactive' + +interface Iprops { + closeCallback: () => void, + isOpen: boolean, +} + +const About = (props: Iprops) => { + return ( + { + props.closeCallback() + }} + ShowX={true} + ShowCancel={true} + CancelBtnClass={"btn btn-danger"} + ShowConfirm={false} + > +

    Version 3.0

    + +

    openSEE is a browser-based waveform display and analytics tool that is used to view waveforms recorded by DFRs, Power Quality meters, relays and other substation devices that are stored in the openXDA database. + The link in the URL window of openSEE can be embedded in emails so that recipients can quickly access the waveforms being studied.

    + +

    General Navigation Features

    + +

    The navigational context of openSEE is relative to the "waveform-of-focus" -- the waveform displayed in the top-most collection of charts that is displayed when openSEE is first opened -- + typically after clicking a link to drill down into a specific waveform in the Open PQ Dashboard. + Tools in openSEE allow the user to dig deeper and understand more about this waveform-of-focus. + Tools in openSEE also enable users to easily change the waveform-of-focus from the initially loaded -- moving forward or back sequentially in time. +

    + +
      +
    • Region Select Zooming - The waveform initially loads with the the time-scale set to the full length of the waveform capture. With the mouse, the user can select a region of the waveform to zoom in and see more detail.
    • +
    • Forward and Back Navigation - Using the collection of controls in the upper-right of the openSEE display, the user can select the basis for changing to a new waveform-of-focus. A selection of "system" means that user can step forward or back + to next event in the openXDA base globally (for all DFRs, PQ Meters, etc.), + i.e., what happened immediately previously or next on the system relative to the current waveform-of-focus. A selection of "asset" (or "line") limits this navigation to just events on this asset. + A selection of "meter" limits this navigation to just events recorded by this substation device.
    • +
    • Chart Trace Section - To the right of each chart, the user has the ability to turn on and off individual traces. Tabs are provided to organize these selections by data type.
    • +
    + +

    + The open-source code for openSEE can be found on GitHub. See: https://github.com/GridProtectionAlliance/openSEE +

    +
    + ); +} + +export default About; \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/InfoSection.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/InfoSection.tsx new file mode 100644 index 00000000..5385e4a1 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/InfoSection.tsx @@ -0,0 +1,107 @@ +//****************************************************************************************************** +// InfoSection.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/09/2026 - Gabriel Santos +// Moved code to here from OpenSEENavbar.tsx +// +//****************************************************************************************************** + +import { ToolTip } from '@gpa-gemstone/react-forms'; +import moment from "moment"; +import React from "react"; +import EventContext from '../Context/EventContext'; + +interface InfoSectionProps { + width: number +} + +const InfoSection = (props: InfoSectionProps) => { + const evt = React.useContext(EventContext); + const [hover, setHover] = React.useState('None'); + + return ( +
    +
      +
    • setHover('Meter')} + onMouseLeave={() => setHover('None')} + data-tooltip={'meter'} + style={{ borderLeft: '1px solid #ddd', borderRight: '1px solid #ddd', paddingLeft: '30px', paddingRight: '30px', flex: '1 1 0', minWidth: 0, overflow: 'hidden' }} + > +
      Meter:
      +
      {evt.Context.EventInfo?.MeterName}
      + +

      {evt.Context.EventInfo?.MeterName}

      +
      +
    • +
    • setHover('Station')} + onMouseLeave={() => setHover('None')} + data-tooltip={'station'} + style={{ borderLeft: '1px solid #ddd', borderRight: '1px solid #ddd', paddingLeft: '30px', paddingRight: '30px', flex: '1 1 0', minWidth: 0, overflow: 'hidden' }} + > +
      Substation:
      +
      {evt.Context.EventInfo?.StationName}
      + +

      {evt.Context.EventInfo?.StationName}

      +
      +
    • +
    • setHover('Asset')} + onMouseLeave={() => setHover('None')} + data-tooltip={'asset'} + style={{ borderLeft: '1px solid #ddd', borderRight: '1px solid #ddd', paddingLeft: '30px', paddingRight: '30px', flex: '1 1 0', minWidth: 0, overflow: 'hidden' }} + > +
      Asset:
      +
      {evt.Context.EventInfo?.AssetName}
      + +

      {evt.Context.EventInfo?.AssetName}

      +
      +
    • +
    • setHover('EType')} + onMouseLeave={() => setHover('None')} + data-tooltip={'etype'} + style={{ borderLeft: '1px solid #ddd', borderRight: '1px solid #ddd', paddingLeft: '15px', paddingRight: '15px', flex: '1 1 0', minWidth: 0, overflow: 'hidden' }}> +
      Type:
      +
      {evt.Context.EventInfo?.EventName}
      + +

      {evt.Context.EventInfo?.EventName}

      +
      +
    • + {props.width > 1695 ? +
    • setHover('EInception')} + onMouseLeave={() => setHover('None')} + data-tooltip={'einception'} + style={{ borderLeft: '1px solid #ddd', borderRight: '1px solid #ddd', paddingLeft: '15px', paddingRight: '15px', flex: '1 1 0', minWidth: 0, overflow: 'hidden' }} + > +
      Inception:
      +
      + {moment(evt.Context.EventInfo?.Inception).format('YYYY-MM-DD HH:mm:ss.SSS')} +
      + +

      {moment(evt.Context.EventInfo?.Inception).format('YYYY-MM-DD HH:mm:ss.SSS')}

      +
      +
    • : null} +
    +
    + ) +} + +export default InfoSection; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/NavStyles.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/NavStyles.ts new file mode 100644 index 00000000..33b1acd0 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/NavStyles.ts @@ -0,0 +1,19 @@ +import * as React from 'react'; + +const navIconSize = 46; + +export const navIconButtonStyle: React.CSSProperties = { + alignItems: 'center', + display: 'inline-flex', + flex: `0 0 ${navIconSize}px`, + height: navIconSize, + justifyContent: 'center', + width: navIconSize +}; + +export const navIconDropdownStyle: React.CSSProperties = { + height: navIconSize, + width: navIconSize * .75 +}; + +export const navIconDropdownButtonClass = 'd-inline-flex align-items-center justify-content-center'; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/Navigation.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/Navigation.tsx new file mode 100644 index 00000000..589661e4 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/Navigation.tsx @@ -0,0 +1,170 @@ +//****************************************************************************************************** +// Navigation.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/09/2026 - Gabriel Santos +// Moved code to here from OpenSEENavbar.tsx +// +//****************************************************************************************************** + +import { ToolTip } from '@gpa-gemstone/react-forms'; +import React from "react"; +import { OpenSee } from "../global"; +import { useAppDispatch, useAppSelector } from "../hooks"; +import { SelectNavigation, SetNavigation } from "../Store/settingSlice"; +import { EventContext } from '../Context/EventContext'; + +const Navigation = () => { + const dispatch = useAppDispatch(); + const navigation = useAppSelector(SelectNavigation); + const evt = React.useContext(EventContext); + const [hover, setHover] = React.useState('None'); + + return ( + <> + {evt.Context.LookupInfo != null ? +
  • +
    +
    setHover('NavLeft')} + onMouseLeave={() => setHover('None')} + data-tooltip={'back-btn'} + > + + +

    Navigate to Previous Event in the {navigation}

    + {navigation === "system" && (

    ({(evt.Context.LookupInfo?.System?.Item1 != null ? evt.Context.LookupInfo.System.Item1.StartTime : '')})

    )} + {navigation === "station" && (

    ({(evt.Context.LookupInfo?.Station?.Item1 != null ? evt.Context.LookupInfo.Station.Item1.StartTime : '')})

    )} + {navigation === "meter" && (

    ({(evt.Context.LookupInfo?.Meter?.Item1 != null ? evt.Context.LookupInfo.Meter.Item1.StartTime : '')})

    )} + {navigation === "asset" && (

    ({(evt.Context.LookupInfo?.Asset?.Item1 != null ? evt.Context.LookupInfo.Asset.Item1.StartTime : '')})

    )} +
    + + {(navigation == "system" ? + + < + : null)} + + {(navigation == "station" ? + + < + + : null)} + + {(navigation == "meter" ? + + < + : null)} + + {(navigation == "asset" ? + + < + : null)} + +
    + +
    setHover('NavRight')} + onMouseLeave={() => setHover('None')} + data-tooltip={'next-btn'} + > + +

    Navigate to Next Event in the {navigation}

    + {navigation === "system" && (

    ({evt.Context.LookupInfo?.System?.Item2 != null ? evt.Context.LookupInfo.System.Item2.StartTime : ''})

    )} + {navigation === "station" && (

    ({(evt.Context.LookupInfo?.Station?.Item2 != null ? evt.Context.LookupInfo.Station.Item2.StartTime : '')})

    )} + {navigation === "meter" && (

    ({(evt.Context.LookupInfo?.Meter?.Item2 != null ? evt.Context.LookupInfo.Meter.Item2.StartTime : '')})

    )} + {navigation === "asset" && (

    ({(evt.Context.LookupInfo?.Asset?.Item2 != null ? evt.Context.LookupInfo.Asset.Item2.StartTime : '')})

    )} +
    + {(navigation == "system" ? + + > + : null)} + {(navigation == "station" ? + + > + : null)} + {(navigation == "meter" ? + + > + : null)} + {(navigation == "asset" ? + + > + : null)} +
    +
    +
  • : null} + + ) +} + +export default Navigation; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/OpenSEENavbar.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/OpenSEENavbar.tsx new file mode 100644 index 00000000..df35b485 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/OpenSEENavbar.tsx @@ -0,0 +1,97 @@ +//****************************************************************************************************** +// OpenSEENavbar.tsx - Gbtc +// +// Copyright © 2019, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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/14/2019 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** +import { clone } from 'lodash'; +import * as React from 'react'; +import { OpenSee } from '../global'; +import { useAppDispatch, useAppSelector } from '../hooks'; +import { SelectMouseMode, SetMouseMode } from '../Store/settingSlice'; +import { IPlotLifecycleActions } from '../Hooks/usePlotLifeCycle'; +import InfoSection from './InfoSection'; +import PlotUtilitiesSection from './PlotUtilitiesSection'; +import WidgetSection from './WidgetSection'; + +interface IProps { + ToggleDrawer: (drawer: OpenSee.OverlayDrawers, open: boolean) => void, + OpenDrawers: OpenSee.Drawers, + Width: number, + lifecycle: IPlotLifecycleActions +} + +const OpenSeeNavBar = (props: IProps) => { + const dispatch = useAppDispatch(); + const mouseMode = useAppSelector(SelectMouseMode); + const [showAbout, setShowAbout] = React.useState(false); + + React.useEffect(() => { + if (props.OpenDrawers.AccumulatedPoints) { + const oldMode = clone(mouseMode); + dispatch(SetMouseMode('select')); + return () => { dispatch(SetMouseMode(oldMode)); }; + } + return () => { }; + }, [props.OpenDrawers.AccumulatedPoints, mouseMode, dispatch]); + + return ( + <> + +
    + {(props.Width < 1568 && props.Width > 1200) || props.Width < 1050 ? + <> +
      + setShowAbout(item)} + OpenDrawers={props.OpenDrawers} + ToggleDrawer={props.ToggleDrawer} + /> +
    +
      + +
    + : + <> +
      + + setShowAbout(item)} + OpenDrawers={props.OpenDrawers} + ToggleDrawer={props.ToggleDrawer} + /> +
    + + } +
    + + ); +}; + +export default OpenSeeNavBar; \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/PlotUtilitiesSection.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/PlotUtilitiesSection.tsx new file mode 100644 index 00000000..50517525 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/PlotUtilitiesSection.tsx @@ -0,0 +1,179 @@ +//****************************************************************************************************** +// PlotUtilitiesSection.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/09/2026 - Gabriel Santos +// Moved code to here from OpenSEENavbar.tsx +// +//****************************************************************************************************** +import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; +import { BtnDropdown } from '@gpa-gemstone/react-interactive'; +import { ToolTip } from '@gpa-gemstone/react-forms'; +import React from "react"; +import About from './About'; +import { OpenSee } from "../global"; +import { useAppDispatch, useAppSelector } from '../hooks'; +import { SelectMouseMode, SetMouseMode, SetZoomMode } from '../Store/settingSlice'; +import Navigation from './Navigation'; +import { PlotDataStateContext } from '../Context/PlotDataContext'; +import { PlotStateStateContext, PlotStateActionContext } from '../Context/PlotStateContext'; +import EventContext from '../Context/EventContext'; +import { selectFFTEnabled } from '../PlotSelectors'; +import { navIconButtonStyle, navIconDropdownButtonClass, navIconDropdownStyle } from './NavStyles'; + +interface IPlotUtilities { + OpenDrawers: OpenSee.Drawers, + showAbout: boolean, + ToggleDrawer: (drawer: OpenSee.OverlayDrawers, open: boolean) => void, + setShowAbout: (about: boolean) => void +} + +const PlotUtilitiesSection = (props: IPlotUtilities) => { + const dispatch = useAppDispatch(); + const { plots } = React.useContext(PlotDataStateContext); + const { meta } = React.useContext(PlotStateStateContext); + const stateActions = React.useContext(PlotStateActionContext); + const evt = React.useContext(EventContext); + + const mouseMode = useAppSelector(SelectMouseMode); + const showFFT = React.useMemo(() => selectFFTEnabled(meta), [meta]); + const [hover, setHover] = React.useState('None'); + const selectDisabled = !props.OpenDrawers.AccumulatedPoints && !props.OpenDrawers.ToolTipDelta; + + return ( + <> +
  • +
    + } + Callback={() => dispatch(SetMouseMode("zoom"))} + Size={'sm'} + Options={[ + { + Label: <> Time, + Callback: () => { dispatch(SetZoomMode('x')); dispatch(SetMouseMode("zoom")); } + }, + { + Label: <> Value, + Callback: () => { dispatch(SetZoomMode('y')); dispatch(SetMouseMode("zoom")); } + }, + { + Label: <> Rectangle, + Callback: () => { dispatch(SetZoomMode('xy')); dispatch(SetMouseMode("zoom")); } + } + ]} + BtnClass={'btn-primary ' + navIconDropdownButtonClass + (mouseMode == "zoom" ? " active" : "")} + ContainerStyle={navIconDropdownStyle} + TooltipContent={

    Zoom

    } + TooltipLocation={'bottom'} + ShowToolTip={true} + /> + + + +

    Pan

    +
    + + + +

    Select

    +
    + + + +

    FFT Move

    +
    + + + +

    Reset Zoom

    +
    +
    +
  • + +
  • + + +

    Settings

    +
    +
  • + + + +
  • + + +

    Help

    +
    + props.setShowAbout(false)} /> +
  • + + ); +}; + +export default PlotUtilitiesSection; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/WidgetSection.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/WidgetSection.tsx new file mode 100644 index 00000000..70b7eb5e --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Navbar/WidgetSection.tsx @@ -0,0 +1,254 @@ +//****************************************************************************************************** +// WidgetSection.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/09/2026 - Gabriel Santos +// Moved code to here from OpenSEENavbar.tsx +// +//****************************************************************************************************** +import { ToggleSwitch, ToolTip } from '@gpa-gemstone/react-forms'; +import { BtnDropdown } from '@gpa-gemstone/react-interactive'; +import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; +import React from "react"; +import AnalyticContext from '../Context/AnalyticContext'; +import { PlotStateStateContext } from '../Context/PlotStateContext'; +import EventContext from '../Context/EventContext'; +import { OverlappingStateContext } from '../Context/OverlappingContext'; +import { OpenSee } from "../global"; +import { useAppDispatch } from '../hooks'; +import { SetMouseMode } from '../Store/settingSlice'; +import { selectFFTEnabled, selectDisplayed, selectAnalytics, selectEventIDs } from '../PlotSelectors'; +import { IPlotLifecycleActions } from '../Hooks/usePlotLifeCycle'; +import { BasePlots } from '../defaults'; +import { navIconButtonStyle, navIconDropdownButtonClass, navIconDropdownStyle } from './NavStyles'; + +interface IWidgets { + OpenDrawers: OpenSee.Drawers, + ToggleDrawer: (drawer: OpenSee.OverlayDrawers, open: boolean) => void, + lifecycle: IPlotLifecycleActions +} + +const WidgetSection = (props: IWidgets) => { + const dispatch = useAppDispatch(); + const evt = React.useContext(EventContext); + const overlapping = React.useContext(OverlappingStateContext); + const [analytic] = React.useContext(AnalyticContext); + const plotState = React.useContext(PlotStateStateContext); + + const [hover, setHover] = React.useState('None'); + + const showFFT = React.useMemo(() => selectFFTEnabled(plotState.meta), [plotState.meta]); + const showPlots = React.useMemo(() => selectDisplayed(plotState.meta), [plotState.meta]); + + const togglePlots = (type: OpenSee.graphType) => { + let display: boolean | undefined; + if (type === 'Voltage') display = showPlots.Voltage; + else if (type === 'Current') display = showPlots.Current; + else if (type === 'Analogs') display = showPlots.Analogs; + else if (type === 'Digitals') display = showPlots.Digitals; + else if (type === 'TripCoil') display = showPlots.TripCoil; + + const eventIds = selectEventIDs(evt.Context.EventID, overlapping.events); + + if (display) + eventIds.forEach(id => props.lifecycle.RemovePlot({ DataType: type, EventId: id })); + else + eventIds.forEach(id => props.lifecycle.AddPlot({ DataType: type, EventId: id })); + } + + const exportData = (type: string) => { + const analytics = selectAnalytics(plotState.meta, evt.Context.EventID); + const uri = homePath + `api/CSV/Download?type=${type}&eventID=${evt.Context.EventInfo?.EventId}` + + `${showPlots.Voltage != undefined ? `&displayVolt=${showPlots.Voltage}` : ``}` + + `${showPlots.Current != undefined ? `&displayCur=${showPlots.Current}` : ``}` + + `${showPlots.TripCoil != undefined ? `&displayTCE=${showPlots.TripCoil}` : ``}` + + `${showPlots.Digitals != undefined ? `&breakerdigitals=${showPlots.Digitals}` : ``}` + + `${showPlots.Analogs != undefined ? `&displayAnalogs=${showPlots.Analogs}` : ``}` + + `${`&displayAnalytics=${analytics}`}` + + `${`&lpfOrder=${analytic.LPFOrder}`}` + + `${`&hpfOrder=${analytic.HPFOrder}`}` + + `${`&Trc=${analytic.Trc}`}` + + `${`&harmonic=${analytic.Harmonic}`}` + + `${type == 'fft' ? `&startDate=${plotState.fftLimits[0]}` : ``}` + + `${type == 'fft' ? `&cycles=${analytic.FFTCycles}` : ``}` + + `&Meter=${evt.Context.EventInfo?.MeterName}` + + `&EventType=${evt.Context.EventInfo?.EventName}`; + window.open(uri, '_blank'); + }; + + const optionList = [ + { Label: "Export CSV", Callback: () => exportData('csv') }, + { Label: "Export PQDS", Callback: () => exportData('pqds') } + ]; + + if (showFFT) + optionList.push({ Label: "Export FFT", Callback: () => exportData('fft') }); + + const waveformOptionList = BasePlots.map(type => { + const enabled = showPlots[type] === true; + const label = type === 'TripCoil' ? 'Trip Coil E.' : type; + + return { + Label: ( +
    +
    +
    + + Record={{ Enabled: enabled }} + Field={'Enabled'} + Label={label} + Setter={() => togglePlots(type)} + Style={{ marginBottom: 0 }} + /> +
    +
    +
    + ), + Callback: () => { } + }; + }); + + const statsOptionList = [ + { Label: "Event Stats", Callback: () => props.ToggleDrawer('EventStats', !props.OpenDrawers.EventStats) } + ]; + + if (evt.Context.EventInfo?.EventName === "Snapshot") + statsOptionList.push({ Label: "Harmonic Stats", Callback: () => props.ToggleDrawer('HarmonicStats', !props.OpenDrawers.HarmonicStats) }); + + return ( + <> +
  • +
  • +
    + } + Callback={() => togglePlots('Voltage')} + Size={'sm'} + Options={waveformOptionList} + ShowToolTip={true} + BtnClass={'btn-primary ' + navIconDropdownButtonClass} + ContainerStyle={navIconDropdownStyle} + TooltipContent={

    Waveform Views

    } + TooltipLocation={'bottom'} + /> +
    +
  • + +
  • + + +

    Show Points

    +
    +
  • + +
  • + + +

    Phasor Chart

    +
    +
  • + +
  • +
    + } + Callback={() => props.ToggleDrawer('EventStats', !props.OpenDrawers.EventStats)} + Size={'sm'} + Options={statsOptionList} + ShowToolTip={true} + BtnClass={'btn-primary ' + navIconDropdownButtonClass} + ContainerStyle={navIconDropdownStyle} + TooltipContent={

    Stats

    } + TooltipLocation={'bottom'} + /> +
    +
  • + +
  • + + +

    Correlated Sags

    +
    +
  • + +
  • + + +

    FFT Table

    +
    +
  • + +
  • + + +

    Lightning Data

    +
    +
  • + +
  • +
    + } + Callback={() => exportData('csv')} + Size={'sm'} + Options={optionList} + ShowToolTip={hover == "Export"} + BtnClass={'btn-primary ' + navIconDropdownButtonClass} + ContainerStyle={navIconDropdownStyle} + TooltipContent={

    Export

    } + /> +
    +
  • + + ); +}; + +export default WidgetSection; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/OpenSeeApplication.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/OpenSeeApplication.tsx new file mode 100644 index 00000000..4ded6594 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/OpenSeeApplication.tsx @@ -0,0 +1,645 @@ +//****************************************************************************************************** +// openSEE.tsx - Gbtc +// +// Copyright � 2018, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 04/17/2018 - Billy Ernest +// Generated original version of source code. +// 08/22/2019 - Christoph Lackner +// Added TCE Plot. +// +//****************************************************************************************************** + +// To-DO: +// # Fix Dowload.ash to include Analytics +// + +import { Application, SplitDrawer, SplitSection, VerticalSplit, IApplicationRefs } from '@gpa-gemstone/react-interactive'; +import { ErrorBoundary } from '@gpa-gemstone/common-pages'; +import createHistory from "history/createBrowserHistory"; +import * as _ from "lodash"; +import moment from 'moment'; +import * as React from 'react'; +import AnalyticOptions from './Components/AnalyticOptions'; +import OverlappingEventWindow from './Components/OverlappingEvents'; +import AnalyticContext from './Context/AnalyticContext'; +import queryString from 'query-string'; +import { EventContext } from './Context/EventContext'; +import { PlotDataStateContext } from './Context/PlotDataContext'; +import { PlotStateStateContext, PlotStateActionContext } from './Context/PlotStateContext'; +import { OverlappingStateContext, OverlappingActionContext } from './Context/OverlappingContext'; +import BarChart from './Graphs/BarChart'; +import LineChart from './Graphs/LineChart'; +import OpenSeeNavBar from './Navbar/OpenSEENavbar'; +import { OpenSee } from './global'; +import { useAppDispatch, useAppSelector } from './hooks'; +import { usePlotLifecycle } from './Hooks/usePlotLifeCycle'; +import { selectListGraphs, selectPlotKeys, selectDisplayed, selectEnabledPlots, selectFFTEnabled } from './PlotSelectors'; +import PointWidget from './Widgets/AccumulatedPoints'; +import EventInfo from './Widgets/EventInfo/EventInfo'; +import FFTTable from './Widgets/FFTTable'; +import HarmonicStatsWidget from './Widgets/HarmonicStats'; +import LightningDataWidget from './Widgets/LightningData'; +import PhasorChartWidget from './Widgets/PhasorChart'; +import EventStatsWidget from './Widgets/EventStats'; +import SettingsWidget from './Widgets/PlotSettings/SettingWindow'; +import TimeCorrelatedSagsWidget from './Widgets/TimeCorrelatedSags'; +import ToolTipWidget from './Widgets/Tooltip'; +import ToolTipDeltaWidget from './Widgets/TooltipWithDelta'; +import { SelectMouseMode, SetMouseMode, SetSinglePlot, SelectSinglePlot } from './Store/settingSlice'; +import { useGetContainerPosition } from '@gpa-gemstone/helper-functions'; + +const OpenSeeApplication = React.memo(() => { + const dispatch = useAppDispatch(); + + const history = React.useRef(createHistory()); + const plotRef = React.useRef(null); + const applicationRef = React.useRef(null); + const overlayHandles = React.useRef({ + Settings: () => { /* noop */ }, + AccumulatedPoints: () => { /* noop */ }, + PolarChart: () => { /* noop */ }, + EventStats: () => { /* noop */ }, + CorrelatedSags: () => { /* noop */ }, + Lightning: () => { /* noop */ }, + FFTTable: () => { /* noop */ }, + HarmonicStats: () => { /* noop */ }, + }); + + const evt = React.useContext(EventContext); + const { plots: plotData } = React.useContext(PlotDataStateContext); + const plotState = React.useContext(PlotStateStateContext); + const stateActions = React.useContext(PlotStateActionContext); + const overlapping = React.useContext(OverlappingStateContext); + const overlappingActions = React.useContext(OverlappingActionContext); + const [analytic, setAnalytic] = React.useContext(AnalyticContext); + + const lifecycle = usePlotLifecycle(); + + const mouseMode = useAppSelector(SelectMouseMode); + const singlePlot = useAppSelector(SelectSinglePlot); + + // Refs for values read inside the history listener callback. + // The listener is set up once on mount, so it would otherwise capture stale closures. + const plotStateRef = React.useRef(plotState); + const plotDataRef = React.useRef(plotData); + const lifecycleRef = React.useRef(lifecycle); + const stateActionsRef = React.useRef(stateActions); + const analyticRef = React.useRef(analytic); + plotStateRef.current = plotState; + plotDataRef.current = plotData; + lifecycleRef.current = lifecycle; + stateActionsRef.current = stateActions; + analyticRef.current = analytic; + + const groupedKeys = React.useMemo(() => selectListGraphs(plotState.meta, singlePlot), [plotState.meta, singlePlot]); + const plotKeys = React.useMemo(() => selectPlotKeys(plotState.meta, singlePlot), [plotState.meta, singlePlot]); + + const [openDrawers, setOpenDrawers] = React.useState({ + Settings: false, + AccumulatedPoints: false, + PolarChart: false, + EventStats: false, + CorrelatedSags: false, + Lightning: false, + FFTTable: false, + Info: false, + Compare: false, + Analytics: false, + ToolTip: false, + ToolTipDelta: false, + HarmonicStats: false + }); + + const { width: plotWidth, height: plotAreaHeight } = useGetContainerPosition(plotRef); + const plotCount = Math.min(plotKeys.length, 3); + const plotHeight = plotCount > 0 ? plotAreaHeight / plotCount : plotAreaHeight; + const [navWidth, setNavWidth] = React.useState(100); + + // Analytic change tracking ref + const oldAnalyticRef = React.useRef<{ [key: string]: number }>({}); + + // Load overlapping events when eventID changes + React.useEffect(() => { + if (evt.Context.EventID > 0) + overlappingActions.LoadOverlappingEvents(evt.Context.EventID); + }, [evt.Context.EventID]); + + // Refresh analytic plots when analytic settings change + React.useEffect(() => { + if (evt.Context.EventID < 0) return; + + Object.keys(analytic).forEach(key => { + if (oldAnalyticRef.current[key] != null && oldAnalyticRef.current[key] !== analytic[key]) { + let graphType: OpenSee.graphType | undefined; + switch (key) { + case 'FFTCycles': + case 'FFTStartTime': graphType = 'FFT'; break; + case 'LPFOrder': graphType = 'LowPassFilter'; break; + case 'HPFOrder': graphType = 'HighPassFilter'; break; + case 'Trc': graphType = 'Rectifier'; break; + case 'Harmonic': graphType = 'Harmonic'; break; + default: + console.warn(`Unrecognized analytic key: ${key}`); + break; + } + if (graphType != null) { + const eventIds = [evt.Context.EventID]; + overlapping.events.forEach(e => { if (e.Selected) eventIds.push(e.EventID); }); + eventIds.forEach(id => lifecycle.UpdateAnalyticPlot({ DataType: graphType!, EventId: id })); + } + } + oldAnalyticRef.current[key] = analytic[key]; + }); + }, [analytic, evt.Context.EventID]); + + // Handle singlePlot toggle + React.useEffect(() => { + if (singlePlot) + lifecycle.RebuildSinglePlots(); + else + lifecycle.RemoveSinglePlots(); + }, [singlePlot]); + + const queryStr = React.useMemo(() => { + const overlappingEvts: number[] = []; + const enabledPlots = selectEnabledPlots(plotState.meta, plotData); + + overlapping.events.forEach(e => { + if (e.Selected) + overlappingEvts.push(e.EventID); + }); + + enabledPlots.forEach(plot => { + const plotEventId = plot.key.EventId; + if (plotEventId !== evt.Context.EventID && plotEventId !== -1) + overlappingEvts.push(plotEventId); + }); + + const plotBase64 = btoa(JSON.stringify(enabledPlots)); + const overlappingBase64 = btoa(JSON.stringify(overlappingEvts)); + + const queryObj = { + eventID: evt.Context.EventID, + startTime: plotState.startTime, + endTime: plotState.endTime, + Trc: analytic.Trc, + HPFOrder: analytic.HPFOrder, + LPFOrder: analytic.LPFOrder, + CycleLimits: plotState.cycleLimits, + FFTLimits: plotState.fftLimits, + FFTCycles: analytic.FFTCycles, + FFTStartTime: analytic.FFTStartTime, + Harmonic: analytic.Harmonic, + singlePlot: singlePlot, + plots: plotBase64, + overlappingInfo: overlappingBase64 + }; + + let query = queryString.stringify(queryObj); + // Trim query string if too long + const plotQuery = [...enabledPlots]; + while (query?.length > 3000 && plotQuery?.length > 0) { + plotQuery.pop(); + queryObj.plots = btoa(JSON.stringify(plotQuery)); + query = queryString.stringify(queryObj); + } + return query; + }, [evt.Context.EventID, plotState, plotData, analytic, singlePlot, overlapping.events]); + + const ToggleDrawer = (drawer: OpenSee.OverlayDrawers, open: boolean) => { + overlayHandles.current[drawer](open); + }; + + const handleDrawerChange = (drawerName: keyof OpenSee.Drawers, isOpen: boolean) => { + setOpenDrawers(prevStates => ({ ...prevStates, [drawerName]: isOpen })); + }; + + const exportData = (type: string) => { + const showPlots = selectDisplayed(plotState.meta); + const uri = homePath + `api/CSV/Download?type=${type}&eventID=${evt.Context.EventID}` + + `${showPlots.Voltage != undefined ? `&displayVolt=${showPlots.Voltage}` : ``}` + + `${showPlots.Current != undefined ? `&displayCur=${showPlots.Current}` : ``}` + + `${showPlots.TripCoil != undefined ? `&displayTCE=${showPlots.TripCoil}` : ``}` + + `${showPlots.Digitals != undefined ? `&breakerdigitals=${showPlots.Digitals}` : ``}` + + `${showPlots.Analogs != undefined ? `&displayAnalogs=${showPlots.Analogs}` : ``}` + + `${type == 'fft' ? `&startDate=${plotState.fftLimits[0]}` : ``}` + + `${type == 'fft' ? `&cycles=${analytic.FFTCycles}` : ``}` + + `&Meter=${evt.Context.EventInfo?.MeterName}` + + `&EventType=${evt.Context.EventInfo?.MeterName}`; + window.open(uri, "_blank"); + } + + const DispatchQuery = (argQuery: string, initial: boolean) => { + // Read current values from refs to avoid stale closures in the history listener + const curPlotState = plotStateRef.current; + const curPlotData = plotDataRef.current; + const curLifecycle = lifecycleRef.current; + const curStateActions = stateActionsRef.current; + const curAnalytic = analyticRef.current; + + const parsedQuery: OpenSee.Query = queryString.parse(argQuery.substring(1)) as unknown as OpenSee.Query; + + let parsedPlots: OpenSee.PlotQuery[] = []; + if (parsedQuery?.plots != null) { + parsedPlots = JSON.parse(atob(parsedQuery.plots)); + } + + if (parsedQuery?.overlappingInfo != null) { + const parsedOverlappingEventIds = JSON.parse(atob(parsedQuery.overlappingInfo)) + .map(ToInt) + .filter((eventId: number | undefined) => eventId != null); + overlappingActions.SetSelectedEvents(parsedOverlappingEventIds); + } + + const enabledPlots = selectEnabledPlots(curPlotState.meta, curPlotData); + + const parsedSinglePlot = ToBool(parsedQuery?.singlePlot); + if (parsedSinglePlot != null) + dispatch(SetSinglePlot(parsedSinglePlot)); + + const parsedEventID = ToInt(parsedQuery?.eventID); + let usedEventID: number = defaultEventID; + if (parsedEventID != null && !isNaN(parsedEventID) && parsedEventID >= 0 && parsedEventID !== evt.Context.EventID) { + evt.Dispatch.current.SettingsDispatch({ EventID: parsedEventID }); + usedEventID = parsedEventID; + } else if (initial) { + evt.Dispatch.current.SettingsDispatch({ EventID: defaultEventID }); + usedEventID = defaultEventID; + } + + const parsedStart = ToFloat(parsedQuery?.startTime); + const parsedEnd = ToFloat(parsedQuery?.endTime); + if (parsedStart != undefined && parsedEnd != undefined && (curPlotState.startTime != parsedStart || curPlotState.endTime != parsedEnd)) + curStateActions.SetTimeLimit(parsedStart, parsedEnd, curPlotData); + + const parsedFFTCycles = ToInt(parsedQuery?.FFTCycles) ?? curAnalytic.FFTCycles; + let parsedFFTStartTime = ToFloat(parsedQuery.FFTStartTime) ?? curAnalytic.FFTStartTime; + if (parsedStart != undefined && parsedEnd != undefined) { + const fftDuration = parsedFFTCycles * 1 / 60.0 * 1000.0; + const maxFFTStartTime = parsedEnd - fftDuration; + if (parsedFFTStartTime < parsedStart || parsedFFTStartTime > maxFFTStartTime) + parsedFFTStartTime = parsedStart; + } + + const analyticQuery: OpenSee.IAnalyticContext = { + Harmonic: ToInt(parsedQuery?.Harmonic) ?? curAnalytic.Harmonic, + Trc: ToInt(parsedQuery?.Trc) ?? curAnalytic.Trc, + LPFOrder: ToInt(parsedQuery?.LPFOrder) ?? curAnalytic.LPFOrder, + HPFOrder: ToInt(parsedQuery?.HPFOrder) ?? curAnalytic.HPFOrder, + FFTCycles: parsedFFTCycles, + FFTStartTime: parsedFFTStartTime + }; + + const analyticData = queryStringToNums(analyticQuery); + if (!_.isEqual(curAnalytic, analyticQuery) && analyticData != null) + setAnalytic(analyticData); + + if (initial && (parsedPlots == null || (parsedPlots?.length === 0 && enabledPlots?.length === 0))) { + curLifecycle.AddPlot({ EventId: usedEventID, DataType: "Voltage" }, undefined, undefined, undefined, undefined, parsedSinglePlot); + curLifecycle.AddPlot({ EventId: usedEventID, DataType: "Current" }, undefined, undefined, undefined, undefined, parsedSinglePlot); + } else if (parsedPlots?.length > 0) { + parsedPlots.forEach(plot => { + const plotChange = parsedPlots.length !== enabledPlots.length; + const oldPlot = enabledPlots.find(p => p.key.DataType === plot.key.DataType && p.key.EventId === plot.key.EventId); + const isYLimitsEqual = _.isEqual(plot?.yLimits, oldPlot?.yLimits); + const isFFTLimitsEqual = _.isEqual([ToInt(parsedQuery?.FFTLimits?.[0]), ToInt(parsedQuery?.FFTLimits?.[1])], curPlotState.fftLimits); + const isCycleLimitsEqual = _.isEqual([ToInt(parsedQuery?.CycleLimits?.[0]), ToInt(parsedQuery?.CycleLimits?.[1])], curPlotState.cycleLimits); + + const fftStart = ToInt(parsedQuery?.FFTLimits?.[0]); + const fftEnd = ToInt(parsedQuery?.FFTLimits?.[1]); + const fftLimits: [number, number] | undefined = + fftStart != null && fftEnd != null && !isFFTLimitsEqual ? [fftStart, fftEnd] : undefined; + + const cycleStart = ToInt(parsedQuery?.CycleLimits?.[0]); + const cycleEnd = ToInt(parsedQuery?.CycleLimits?.[1]); + const cycleLimits: [number, number] | undefined = + cycleStart != null && cycleEnd != null && !isCycleLimitsEqual ? [cycleStart, cycleEnd] : undefined; + + if (plotChange && oldPlot == null && plot.key.EventId !== -1) { + if (parsedSinglePlot ?? false) { + parsedPlots.filter(p => p.key.EventId !== -1 && p.key.DataType === plot.key.DataType) + .filter(p => enabledPlots.find(ep => ep.key.DataType === p.key.DataType && ep.key.EventId === p.key.EventId) == null) + .forEach(p => curLifecycle.AddPlot( + p.key, + !isYLimitsEqual ? plot.yLimits : undefined, + plot.isZoomed, + fftLimits, + cycleLimits, + parsedSinglePlot + )); + } else { + curLifecycle.AddPlot( + plot.key, + !isYLimitsEqual ? plot.yLimits : undefined, + plot.isZoomed, + fftLimits, + cycleLimits + ); + } + } + }); + } + } + + // Resize effects + React.useLayoutEffect(() => { + const navBar = applicationRef.current?.navBarDiv; + if (navBar == null) return; + + const updateNavWidth = () => { + const newNavBarWidth = navBar.getBoundingClientRect().width; + + if (!isNaN(newNavBarWidth) && isFinite(newNavBarWidth)) + setNavWidth(newNavBarWidth); + }; + + const resizeObserver = new ResizeObserver(updateNavWidth); + + updateNavWidth(); + resizeObserver.observe(navBar); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + // Reset time limits when a new event finishes loading + React.useEffect(() => { + if (evt.Context.Status !== 'idle' || evt.Context.EventInfo == null) return; + + const startTime = new Date(evt.Context.EventInfo.EventDate + "Z").getTime(); + const endTime = new Date(evt.Context.EventInfo.EventEnd + "Z").getTime(); + + if (!isNaN(startTime) && !isNaN(endTime)) { + stateActions.SetTimeLimit(startTime, endTime, plotData); + setAnalytic(a => { + const fftDuration = a.FFTCycles * 1 / 60.0 * 1000.0; + if (a.FFTStartTime >= startTime && a.FFTStartTime <= endTime - fftDuration) + return a; + return { ...a, FFTStartTime: startTime }; + }); + } + }, [evt.Context.EventID, evt.Context.Status]); + + // Query string effect + React.useEffect(() => { + const query = queryString.parse(history.current['location'].search); + const parsedStartTime = query['startTime'] != undefined ? parseInt(query['startTime'] as string) : undefined; + const parsedEndTime = query['endTime'] != undefined ? parseInt(query['endTime'] as string) : undefined; + + if (parsedStartTime != undefined && parsedEndTime != undefined) { + stateActionsRef.current.SetTimeLimit(parsedStartTime, parsedEndTime, plotDataRef.current); + setAnalytic(a => ({ ...a, FFTStartTime: parsedStartTime })); + } else { + //fallback + const evStart = new Date(defaultEventStartTime + "Z").getTime(); + const evEnd = new Date(defaultEventEndTime + "Z").getTime(); + stateActionsRef.current.SetTimeLimit(evStart, evEnd, plotDataRef.current); + setAnalytic(a => ({ ...a, FFTStartTime: evStart })); + } + + DispatchQuery(history.current['location'].search, true); + + history.current['listen'](location => { + DispatchQuery(location.search, false); + }); + }, []); + + React.useEffect(() => { + const timeoutId = setTimeout(() => { + history.current['push'](`?${queryStr}`); + }, 1000); + return () => clearTimeout(timeoutId); + }, [queryStr]); + + // Tooltip select mode effect + React.useEffect(() => { + if (openDrawers.ToolTipDelta) { + const oldMode = _.clone(mouseMode); + dispatch(SetMouseMode('select')); + return () => { dispatch(SetMouseMode(oldMode)); }; + } + }, [openDrawers.ToolTipDelta]); + + const renderPlot = (item: OpenSee.IGraphProps) => { + if (item.DataType === 'FFT') + return ( + + + + ); + + return ( + + + + ); + }; + + return ( + } + NavBarStyle={{ zIndex: 1051 /* The OverlayDrawer has a zIndex of 1050 and will bleed onto nav when */ }} + NavBarImgStyle={{ maxHeight: 55, margin: -5 }} + UseLegacyNavigation={true} + ref={applicationRef} + > + + handleDrawerChange("Info", item)}> + + + + + + handleDrawerChange("Compare", item)}> + + + + + + handleDrawerChange("Analytics", item)}> + + + + + + handleDrawerChange("ToolTip", item)}> + + + + + + handleDrawerChange("ToolTipDelta", item)}> + + + + + + { overlayHandles.current.Settings = func; }} ShowClosed={false} + OnChange={(item) => handleDrawerChange("Settings", item)}> + + + + + + { overlayHandles.current.AccumulatedPoints = func; }} ShowClosed={false} + OnChange={(item) => handleDrawerChange("AccumulatedPoints", item)}> + + + + + + { overlayHandles.current.EventStats = func; }} ShowClosed={false} + OnChange={(item) => handleDrawerChange("EventStats", item)}> + + + + + + { overlayHandles.current.CorrelatedSags = func; }} ShowClosed={false} + OnChange={(item) => handleDrawerChange("CorrelatedSags", item)}> + + + + + + { overlayHandles.current.Lightning = func; }} ShowClosed={false} + OnChange={(item) => handleDrawerChange("Lightning", item)}> + + + + + + { overlayHandles.current.FFTTable = func; }} ShowClosed={false} + OnChange={(item) => handleDrawerChange("FFTTable", item)}> + + + + + + { overlayHandles.current.PolarChart = func; }} ShowClosed={false} + OnChange={(item) => handleDrawerChange("PolarChart", item)}> + + + + + + { overlayHandles.current.HarmonicStats = func; }} ShowClosed={false} + OnChange={(item) => handleDrawerChange("HarmonicStats", item)}> + + + + + + +
    + {groupedKeys[evt.Context.EventID] != undefined ? ( + <> + {groupedKeys[evt.Context.EventID].map(renderPlot)} + + ) : null} + + {Object.keys(groupedKeys).filter(item => parseInt(item) !== evt.Context.EventID).map(key => +
    + {overlapping.events.find(item => item.EventID === parseInt(key)) ? ( +
    +
    +
    + Meter:
    + {overlapping.events.find(item => item.EventID === parseInt(key))?.MeterName ?? 'n/a'} +
    +
    + Asset:
    + {overlapping.events.find(item => item.EventID === parseInt(key))?.AssetName ?? 'n/a'} +
    +
    + Type:
    + {overlapping.events.find(item => item.EventID === parseInt(key))?.EventType ?? 'n/a'} +
    +
    + Inception:
    + {moment(overlapping.events.find(item => item.EventID === parseInt(key))?.Inception).format('YYYY-MM-DD HH:mm:ss.SSS')} +
    +
    +
    + ) : null} +
    + {groupedKeys[key].map(renderPlot)} +
    +
    + )} +
    +
    +
    +
    + ); +}); + +export default OpenSeeApplication; + +const ToInt = (arg: any) => { + if (arg == undefined) return undefined; + const val = parseInt(arg); + return isNaN(val) ? undefined : val; +} + +const ToFloat = (arg: any) => { + if (arg == undefined) return undefined; + const val = parseFloat(arg); + return isNaN(val) ? undefined : val; +} + +const ToBool = (arg: any) => { + if (arg == undefined) return undefined; + if (arg == "True" || arg == "true" || arg == "1") return true; + if (arg == "False" || arg == "false" || arg == "0") return false; + return undefined; +} + +const queryStringToNums = (arg: OpenSee.IAnalyticContext) => { + if (arg == undefined) return undefined; + const query = {}; + Object.keys(arg).forEach(key => { + const num = parseFloat(arg[key]); + query[key] = isNaN(num) ? arg[key] : num; + }); + return query as OpenSee.IAnalyticContext; +} + +interface IOpenSeeErrorBoundaryProps { + message: string; +} + +const OpenSeeErrorBoundary = (props: React.PropsWithChildren) => ( + + {props.children} + +); diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/PlotSelectors.ts b/src/OpenSEE/wwwroot/Scripts/TSX/PlotSelectors.ts new file mode 100644 index 00000000..f896eb35 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/PlotSelectors.ts @@ -0,0 +1,409 @@ +// PlotSelectors.ts +// Pure functions that derive display-ready data from the plot state and data maps. +// No React, no hooks, no context -- just inputs and outputs. +// Consumers call these in useMemo or inline with data from their context subscriptions. + +import _ from 'lodash'; +import { OpenSee } from './global'; +import { defaultSettings } from './defaults'; +import { PlotKey, toPlotKey, seriesToKey } from './Context/PlotKeys'; +import { IPlotMeta, getIndex, getDisplayName, getPrimaryAxis } from './Context/PlotStateUtilities'; +import { PlotDataMap } from './Context/PlotStateContext'; + +const defaultOption: OpenSee.iUnitOptions = { label: '', factor: 1, short: '' }; + +// Resolve the active y-limits for a plot based on zoom/manual/data state +export function selectYLimits(meta: IPlotMeta): OpenSee.IUnitCollection<[number, number]> { + const result = {} as OpenSee.IUnitCollection<[number, number]>; + Object.keys(meta.yLimits).forEach(unit => { + const s = meta.yLimits[unit]; + if (meta.isZoomed) + result[unit] = s.zoomedLimits; + else if (s.isManual && s.manualLimits) + result[unit] = s.manualLimits; + else + result[unit] = s.dataLimits; + }); + return result; +} + +// Build axis label strings like "Voltage [kV]" +export function selectYLabels(meta: IPlotMeta): OpenSee.IUnitCollection { + const labels = {} as OpenSee.IUnitCollection; + Object.keys(meta.yLimits).forEach(unit => { + const short = defaultSettings.Units[unit]?.options?.[meta.yLimits[unit].current]?.short ?? 'N/A'; + labels[unit] = `${unit} [${short}]`; + }); + return labels; +} + +// Get the active unit option object for each axis on a plot +export function selectActiveUnit(meta: IPlotMeta): Record { + const result: Record = {}; + const baseUnits = defaultSettings.Units; + Object.keys(baseUnits).forEach(unit => { + if (meta.yLimits[unit]) + result[unit] = baseUnits[unit].options[meta.yLimits[unit].current]; + }); + return result; +} + +// Get unique units present in a plot's data, with the primary axis first +export function selectRelevantUnits( + key: OpenSee.IGraphProps, + data: OpenSee.iD3DataSeries[] +): OpenSee.Unit[] { + let units: OpenSee.Unit[] = data.map(d => d.Unit); + const primary = getPrimaryAxis(key); + if (units.includes(primary)) { + units = units.filter(u => u !== primary); + units.unshift(primary); + } + return _.uniq(units); +} + +// Same as above but only for enabled traces +export function selectEnabledUnits( + key: OpenSee.IGraphProps, + data: OpenSee.iD3DataSeries[], + enabled: Record +): OpenSee.Unit[] { + let units: OpenSee.Unit[] = []; + data.forEach(d => { + if (d.Unit != null && enabled[seriesToKey(d)]) units.push(d.Unit); + }); + const primary = getPrimaryAxis(key); + if (units.includes(primary)) { + units = units.filter(u => u !== primary); + units.unshift(primary); + } + return _.uniq(units); +} + +// Which base plot types (Voltage, Current, etc.) are currently displayed +export function selectDisplayed(meta: Record): OpenSee.IDisplayed { + const types = Object.values(meta).map(m => m.key.DataType); + return { + Voltage: types.includes('Voltage'), + Current: types.includes('Current'), + TripCoil: types.includes('TripCoil'), + Analogs: types.includes('Analogs'), + Digitals: types.includes('Digitals') + }; +} + +// Unique plot keys in metadata insertion order +export function selectPlotKeys( + meta: Record, + singlePlot: boolean +): OpenSee.IGraphProps[] { + let keys = Object.values(meta).map(m => m.key); + if (singlePlot) + keys = keys.filter(k => k.EventId === -1); + keys = _.uniqWith(keys, _.isEqual); + return keys; +} + +// Group plot keys by EventId for rendering sections +export function selectListGraphs( + meta: Record, + singlePlot: boolean +): _.Dictionary { + const keys = Object.values(meta).map(m => m.key); + if (singlePlot) + return _.groupBy(keys.filter(k => k.EventId === -1), 'EventId'); + return _.groupBy(keys.filter(k => k.EventId !== -1), 'EventId'); +} + +// Which plot types are analytics (not base waveforms) +export function selectAnalytics( + meta: Record, + eventId: number +): OpenSee.graphType[] { + const analyticTypes: OpenSee.graphType[] = [ + 'FirstDerivative', 'ClippedWaveforms', 'Frequency', 'HighPassFilter', + 'LowPassFilter', 'MissingVoltage', 'OverlappingWave', 'Power', + 'Impedance', 'Rectifier', 'RapidVoltage', 'RemoveCurrent', 'Harmonic', + 'SymetricComp', 'THD', 'Unbalance', 'FaultDistance', 'Restrike', 'I2T' + ]; + return _.uniq( + Object.values(meta) + .filter(m => m.key.EventId === eventId && analyticTypes.includes(m.key.DataType)) + .map(m => m.key.DataType) + ); +} + +// Get all event IDs that are currently active (main + selected overlapping) +export function selectEventIDs( + eventId: number, + overlappingEvents: OpenSee.OverlappingEvents[] +): number[] { + const ids = [eventId]; + overlappingEvents.forEach(e => { + if (e.Selected) ids.push(e.EventID); + }); + return _.uniq(ids); +} + +// Tooltip: get values at hover point for all enabled traces on the main event +export function selectHoverPoints( + hover: [number, number], + eventId: number, + plotData: PlotDataMap, + meta: Record, + harmonic?: number +): OpenSee.IPoint[] { + const result: OpenSee.IPoint[] = []; + + Object.keys(meta).forEach(pk => { + const m = meta[pk]; + if (m.key.EventId !== eventId) return; + const d = plotData[pk] ?? []; + if (d.length === 0) return; + + const firstIndex = getIndex(hover[0], d[0].DataPoints); + if (isNaN(firstIndex)) return; + + d.forEach((series) => { + if (!m.enabled[seriesToKey(series)]) return; + const idx = getIndex(hover[0], series.DataPoints); + const unitOpt = defaultSettings.Units[series.Unit]?.options?.[m.yLimits[series.Unit]?.current] ?? defaultOption; + result.push({ + Color: series.Color, + Unit: unitOpt, + Value: idx > series.DataPoints.length - 1 ? NaN : series.DataPoints[idx][1], + Name: getDisplayName(series, m.key.DataType, harmonic), + BaseValue: series.BaseValue, + Time: 0 + }); + }); + }); + + return result; +} + +// Tooltip with delta: same as hover points but includes previous selected point value +export function selectDeltaHoverPoints( + hover: [number, number], + eventId: number, + plotData: PlotDataMap, + meta: Record, + dataKey?: OpenSee.IGraphProps, + harmonic?: number +): OpenSee.IPoint[] { + const result: OpenSee.IPoint[] = []; + + Object.keys(meta).forEach(pk => { + const m = meta[pk]; + if (m.key.EventId !== eventId) return; + if (dataKey != null && (m.key.DataType !== dataKey.DataType || m.key.EventId !== dataKey.EventId)) return; + + const d = plotData[pk] ?? []; + if (d.length === 0) return; + + const firstIndex = getIndex(hover[0], d[0].DataPoints); + if (isNaN(firstIndex)) return; + + const selIdx = m.selectedIndices; + + d.forEach((series) => { + if (!m.enabled[seriesToKey(series)]) return; + const idx = getIndex(hover[0], series.DataPoints); + const unitOpt = defaultSettings.Units[series.Unit]?.options?.[m.yLimits[series.Unit]?.current] ?? defaultOption; + const lastSel = selIdx.length > 0 ? selIdx[selIdx.length - 1] : -1; + const selectedTime = m.selectedTimes?.length > 0 ? m.selectedTimes[m.selectedTimes.length - 1] : null; + result.push({ + Color: series.Color, + Unit: unitOpt, + Value: idx > series.DataPoints.length - 1 ? NaN : series.DataPoints[idx][1], + Name: getDisplayName(series, m.key.DataType, harmonic), + PrevValue: lastSel >= 0 && lastSel < series.DataPoints.length ? series.DataPoints[lastSel][1] : NaN, + BaseValue: series.BaseValue, + Time: selectedTime ?? (lastSel >= 0 && lastSel < series.DataPoints.length ? series.DataPoints[lastSel][0] : NaN) + }); + }); + }); + + return result; +} + +// Phasor chart: extract voltage or current phase vectors at hover point +export function selectPhaseVectors( + hover: [number, number], + eventId: number, + dataType: 'Voltage' | 'Current', + plotData: PlotDataMap, + meta: Record +): OpenSee.IVector[] { + const pk = toPlotKey({ DataType: dataType, EventId: eventId }); + const m = meta[pk]; + const d = plotData[pk] ?? []; + + if (!m || d.length === 0 || !d.some(s => s.LegendHorizontal === 'Ph')) + return []; + + const activeUnits = m.yLimits; + const assets = _.uniq(d.filter(s => m.enabled[seriesToKey(s)]).map(s => s.LegendGroup)); + const phases = _.uniq(d.filter(s => m.enabled[seriesToKey(s)]).map(s => s.LegendVertical)); + + const phaseData = d.find(s => s.LegendHorizontal === 'Ph'); + const pointIndex = phaseData ? getIndex(hover[0], phaseData.DataPoints) : -1; + if (isNaN(pointIndex) || pointIndex < 0) return []; + + const unitKey = dataType as OpenSee.Unit; + const unit = defaultSettings.Units[unitKey]?.options?.[activeUnits[unitKey]?.current] ?? defaultOption; + const phaseUnit = defaultSettings.Units.Angle?.options?.[activeUnits['Angle']?.current] ?? defaultOption; + + const result: OpenSee.IVector[] = []; + + assets.forEach(a => { + phases.forEach(p => { + const phCh = d.find(s => s.LegendGroup === a && s.LegendVertical === p && s.LegendHorizontal === 'Ph'); + const magCh = d.find(s => s.LegendGroup === a && s.LegendVertical === p && s.LegendHorizontal === 'Pk'); + if (!phCh || !magCh) return; + + result.push({ + Color: phCh.Color, + Unit: unit, + PhaseUnit: phaseUnit, + Phase: p, + Asset: a, + Magnitude: pointIndex < magCh.DataPoints.length ? magCh.DataPoints[pointIndex][1] : NaN, + Angle: pointIndex < phCh.DataPoints.length ? phCh.DataPoints[pointIndex][1] : NaN, + BaseValue: magCh.BaseValue + }); + }); + }); + + return result; +} + +// Accumulated points widget: get selected point values for V/I traces +export function selectSelectedPoints( + eventId: number, + plotData: PlotDataMap, + meta: Record +): OpenSee.IPointCollection[] { + const result: OpenSee.IPointCollection[] = []; + + Object.keys(meta).forEach(pk => { + const m = meta[pk]; + if (m.key.EventId !== eventId) return; + if (m.key.DataType !== 'Voltage' && m.key.DataType !== 'Current') return; + const d = plotData[pk] ?? []; + if (d.length === 0) return; + + d.forEach((series) => { + if (!m.enabled[seriesToKey(series)]) return; + const unitType = series.Unit; + const unitOpt = defaultSettings.Units[unitType]?.options?.[m.yLimits[unitType]?.current] ?? defaultOption; + + result.push({ + Group: series.LegendGroup, + Name: (m.key.DataType === 'Voltage' ? 'V ' : 'I ') + series.LegendVertical + ' ' + series.LegendHorizontal, + Unit: unitOpt, + Value: m.selectedIndices.map(j => series.DataPoints[j]), + BaseValue: series.BaseValue, + Color: series.Color + }); + }); + }); + + return result; +} + +// FFT table: extract magnitude/angle/frequency series grouped by asset+phase +export function selectFFTData( + eventId: number, + plotData: PlotDataMap, + meta: Record +): OpenSee.IFFTSeries[] { + const pk = toPlotKey({ DataType: 'FFT', EventId: eventId }); + const m = meta[pk]; + const d = plotData[pk] ?? []; + + if (!m || d.length === 0) return []; + + const assets = _.uniq(d.map(s => s.LegendGroup)); + const phases = _.uniq(d.map(s => s.LegendVertical)); + const result: OpenSee.IFFTSeries[] = []; + + assets.forEach(a => { + phases.forEach(p => { + const subset = d.filter(s => s.LegendGroup === a && s.LegendVertical === p); + if (subset.length === 0) return; + + const angCh = subset.find(s => s.LegendHorizontal === 'Ang'); + const magCh = subset.find(s => s.LegendHorizontal === 'Mag'); + if (!angCh || !magCh) return; + + const magUnit = defaultSettings.Units[magCh.Unit]?.options?.[m.yLimits[magCh.Unit]?.current] ?? defaultOption; + const angUnit = defaultSettings.Units.Angle?.options?.[m.yLimits['Angle']?.current] ?? defaultOption; + + result.push({ + Color: angCh.Color, + Unit: magUnit, + PhaseUnit: angUnit, + Phase: p, + Asset: a, + Magnitude: magCh.DataPoints.map(pt => pt[1]), + Angle: angCh.DataPoints.map(pt => pt[1]), + BaseValue: magCh.BaseValue, + Frequency: magCh.DataPoints.map(pt => pt[0] * 60.0) + }); + }); + }); + + return result; +} + +// Whether any FFT plot is currently active +export function selectFFTEnabled(meta: Record): boolean { + return Object.values(meta).some(m => m.key.DataType === 'FFT'); +} + +// Get overlapping event plot keys (plots not belonging to the main event) +export function selectOverlappingPlotKeys( + meta: Record, + eventId: number, + graphType: OpenSee.graphType +): OpenSee.IGraphProps[] { + return _.orderBy( + Object.values(meta) + .filter(m => m.key.EventId !== eventId && m.key.EventId !== -1 && m.key.DataType === graphType) + .map(m => m.key), + 'EventId', + 'desc' + ); +} + +// Build plot query objects for query string serialization +export function selectEnabledPlots( + meta: Record, + plotData: PlotDataMap +): OpenSee.PlotQuery[] { + const result: OpenSee.PlotQuery[] = []; + + Object.keys(meta).forEach(pk => { + const m = meta[pk]; + const d = plotData[pk] ?? []; + + const enabledUnits = _.uniq( + d.filter(s => m.enabled[seriesToKey(s)]).map(s => s.Unit) + ); + + const yLimits = {} as OpenSee.IUnitCollection; + Object.keys(m.yLimits).forEach(unit => { + if (enabledUnits.includes(unit as OpenSee.Unit)) + yLimits[unit] = { ...m.yLimits[unit], autoUnit: m.yLimits[unit].isAuto }; + }); + + result.push({ + key: m.key, + yLimits, + isZoomed: m.isZoomed + }); + }); + + return result; +} diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/AccumulatedPoints.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/AccumulatedPoints.tsx new file mode 100644 index 00000000..e31662f9 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/AccumulatedPoints.tsx @@ -0,0 +1,141 @@ +//****************************************************************************************************** +// AccumulatedPoints.tsx - Gbtc +// +// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/11/2018 - Billy Ernest +// Generated original version of source code. +// +// 01/24/2024 - Preston Crawford +// Fix Remove point button / refactor table layout +// +//****************************************************************************************************** +import * as React from 'react'; +import { SelectColor } from '../Store/settingSlice'; +import { useAppSelector } from '../hooks'; +import { PlotDataStateContext } from '../Context/PlotDataContext'; +import { PlotStateStateContext, PlotStateActionContext } from '../Context/PlotStateContext'; +import EventContext from '../Context/EventContext'; +import { selectSelectedPoints } from '../PlotSelectors'; +import { useGetContainerPosition } from '@gpa-gemstone/helper-functions'; +import { Alert } from '@gpa-gemstone/react-interactive'; + +const PointWidget = () => { + const { plots } = React.useContext(PlotDataStateContext); + const plotState = React.useContext(PlotStateStateContext); + const stateActions = React.useContext(PlotStateActionContext); + const evt = React.useContext(EventContext); + const colors = useAppSelector(SelectColor); + + const points = React.useMemo( + () => selectSelectedPoints(evt.Context.EventID, plots, plotState.meta), + [evt.Context.EventID, plots, plotState.meta] + ); + + const [selectedIndex, setSelectedIndex] = React.useState(-1); + + const flexRef = React.useRef(null); + const { offsetWidth: flexWidth } = useGetContainerPosition(flexRef); + + const firstCellRef = React.useRef(null); + const secondCellRef = React.useRef(null); + const { offsetWidth: firstCellWidth } = useGetContainerPosition(firstCellRef); + const { offsetWidth: secondCellWidth } = useGetContainerPosition(secondCellRef); + + const [leftPosition, setLeftPosition] = React.useState({ secondCell: 0, thirdCell: 0 }); + + React.useLayoutEffect(() => { + setLeftPosition({ secondCell: firstCellWidth, thirdCell: firstCellWidth + secondCellWidth }); + }, [firstCellWidth, secondCellWidth]); + + if (points.length === 0) + return ( +
    +
    + + No data for Accumulated Points. + +
    +
    + ); + + return ( +
    +
    + + + + + + + {points[0]?.Value?.map((p, i) => ( + + ))} + + + + {points.map((point, pointIndex) => ( + + + + + {point.Value.map((p, i) => ( + + ))} + + ))} + +
    +     + + Time + + Value +
    + Delta +
    + + {(p[0] - plotState.startTime).toFixed(7)} sec
    {((p[0] - plotState.startTime) * 60.0).toFixed(2)} cycles +
    +
    +     + + {point.Group} + + Value +
    + Delta +
    setSelectedIndex(i)} style={{ backgroundColor: (selectedIndex === i ? 'yellow' : undefined), textAlign: 'center', verticalAlign: 'middle' }}> + + {(p[1] * (point.Unit?.factor === undefined ? 1.0 / point.BaseValue : point.Unit?.factor)).toFixed(2)} {point?.Unit?.short} + +
    + + {i === 0 ? 'N/A' : + ((point.Value[i - 1][1] - p[1]) * (point.Unit?.factor === undefined ? 1.0 / point.BaseValue : point.Unit?.factor)).toFixed(4)} {point?.Unit?.short} + +
    +
    +
    + { if (selectedIndex !== -1) stateActions.RemoveSelectPoints(selectedIndex); setSelectedIndex(-1); }} /> + stateActions.RemoveSelectPoints(points[0].Value.length - 1)} /> + { stateActions.ClearSelectPoints(); setSelectedIndex(-1); }} /> +
    +
    + ); +}; + +export default PointWidget; \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/EventInfo/EventInfo.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/EventInfo/EventInfo.tsx new file mode 100644 index 00000000..03d54022 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/EventInfo/EventInfo.tsx @@ -0,0 +1,312 @@ +//****************************************************************************************************** +// EventInfo.tsx - Gbtc +// +// Copyright c 2018, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 12/27/2023 - Preston Crawford +// Generated original version of source code. +//****************************************************************************************************** + +import React from 'react'; +import queryString from 'query-string'; +import moment from 'moment'; +import FaultSpecificsModal from './FaultSpecificsModal'; +import EventContext from '../../Context/EventContext'; +import { Column, Table } from '@gpa-gemstone/react-table'; +import { OpenSee } from '../../global'; +import { Application, Gemstone } from '@gpa-gemstone/application-typings'; +import { Alert } from '@gpa-gemstone/react-interactive'; +import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; + +const eventDateFormat = "YYYY-MM-DD HH:mm:ss.fffffff"; +const dateFormat = "MM/DD/YYYY"; +const timeFormat = "HH:mm:ss.SSS"; + +interface TableData extends Gemstone.TSX.Interfaces.ILabelValue { + Key: keyof OpenSee.IEventInfo +} + +const skippedKeys: (keyof OpenSee.IEventInfo)[] = [ + 'Inception', + 'InceptionDate', + 'EventDate', + 'xdaInstance', + 'enableLightningData', + 'LineLength', + 'EventEnd', + 'SystemFrequency', + 'MeterId', + 'DurationEndTime', + 'CalculationCycle', + 'Date', + 'DurationCycles', + 'DurationSeconds' +]; +const EventInfo = () => { + const { Context } = React.useContext(EventContext); + + const [pqBrowserURL, setPqBrowserURL] = React.useState('http://localhost:44368') + const [pqBrowserStatus, setPQBrowserStatus] = React.useState('uninitiated'); + const [pqBrowserParams, setPQBrowserParams] = React.useState("") + const [showFaultSpecifics, setShowFaultSpecifics] = React.useState(false) + + React.useEffect(() => { + setPQBrowserStatus('loading'); + + const handle = getPQUrl(); + + handle.done((data) => { + setPqBrowserURL(data); + setPQBrowserStatus('idle'); + }); + handle.fail(() => setPQBrowserStatus('error')); + + return () => { + if (handle?.abort != null) + handle.abort() + } + }, []); + + React.useEffect(() => { + if (Context.EventInfo?.EventDate == null) + return; + + const time = moment.utc(Context.EventInfo.EventDate, eventDateFormat).format(timeFormat); + const date = moment.utc(Context.EventInfo.EventDate, eventDateFormat).format(dateFormat); + + const queryParams = { + eventid: Context.EventID, + time: time, + date: date, + windowSize: 1, + timeWindowUnits: 3 + }; + + setPQBrowserParams(queryString.stringify(queryParams)) + }, [Context.EventInfo?.EventDate]); + + const tableData = React.useMemo(() => { + if (Context.EventInfo == null) + return []; + + const data: TableData[] = Object.keys(Context.EventInfo) + .filter((key) => !skippedKeys.includes(key as keyof OpenSee.IEventInfo)) + .map((key) => { + const typedKey = key as keyof OpenSee.IEventInfo; + + return { + Label: getLabel(typedKey), + Key: typedKey, + Value: String(Context.EventInfo![typedKey]) + }; + }); + + data.push({ + Label: '', + Key: 'PQBrowser' as keyof OpenSee.IEventInfo, + Value: '' + }); + + return data; + }, [Context.EventInfo, pqBrowserURL, pqBrowserParams]); + + const isLoading = Context.Status === 'loading' || Context.Status === 'uninitiated' || pqBrowserStatus === 'loading' || pqBrowserStatus === 'uninitiated'; + + return ( +
    + {isLoading ? +
    + +
    + : null} + {Context.Status === 'error' ? +
    +
    + + Error retrieving fault information. + +
    +
    + : null} + {pqBrowserStatus === 'error' ? +
    +
    + + Error retrieving PQ Browser URL. + +
    +
    + : null} + {Context.Status === 'idle' && !isLoading && Context.EventInfo == null ? +
    +
    + + No data for Info. + +
    +
    + : null} + {Context.Status === 'idle' && !isLoading && Context.EventInfo != null ? + + Data={tableData} + SortKey={'Key'} + Ascending={true} + OnSort={() => {/* no-op */ }} + TableClass="table" + TbodyStyle={{ overflowY: 'auto' }} + KeySelector={(item) => item.Key} + > + + Key="Label" + Field="Label" + AllowSort={false} + > + {''} + + + + Key="Value" + Field="Value" + AllowSort={false} + Content={({ item }) => ( + <>{getValue(item, Context.EventInfo, setShowFaultSpecifics, pqBrowserURL, pqBrowserParams)} + )} + > + {''} + + + : null} + + {Context.EventInfo != null ? + + : null} +
    + ) +} + +const getPQUrl = () => { + return $.ajax({ + type: "GET", + url: `${homePath}api/OpenSEE/GetPQBrowser/`, + contentType: "application/json; charset=utf-8", + dataType: 'text', + cache: true, + async: true + }); +} + +const getLabel = (key: keyof OpenSee.IEventInfo): string => { + switch (key) { + case 'MeterName': + return 'Meter'; + + case 'StationName': + return 'Substation'; + + case 'AssetName': + return 'Asset'; + + case 'EventName': + return 'Event Type'; + + case 'EventMilliseconds': + return 'Event Date'; + + case 'Inception': + return 'Inception'; + + case 'StartTime': + return 'Record Start Time'; + + case 'Phase': + return 'Phase'; + + case 'DurationPeriod': + return 'Duration Cycles'; + + case 'Magnitude': + return 'Magnitude'; + + case 'SagDepth': + return 'Sag Depth'; + + case 'BreakerNumber': + return 'Breaker'; + + case 'BreakerTiming': + return 'Timing'; + + case 'BreakerSpeed': + return 'Speed'; + + case 'BreakerOperation': + return 'Operation'; + + case 'EventId': + return 'Event ID'; + + default: + return key; + } +} + +const getValue = ( + tableData: TableData, + record: OpenSee.IEventInfo | null, + setShowFaultSpecs: React.Dispatch>, + pqBrowserURL: string, + pqBrowserParams: string +): React.ReactNode => { + if (record == null) + return null; + + switch (tableData.Key) { + case 'EventName': + return record.EventName !== 'Fault' + ? record.EventName + : setShowFaultSpecs(true)} + > + Fault + + + case 'EventMilliseconds': + return moment(Number(tableData.Value)) + .format('YYYY-MM-DD HH:mm:ss.SSS'); + + case 'PQBrowser' as keyof OpenSee.IEventInfo: + return ( + + ); + + default: + return tableData.Value; + } +} + +export default EventInfo; \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/EventInfo/FaultSpecificsModal.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/EventInfo/FaultSpecificsModal.tsx new file mode 100644 index 00000000..d02a4dac --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/EventInfo/FaultSpecificsModal.tsx @@ -0,0 +1,169 @@ +//****************************************************************************************************** +// TimeCorrelatedSags.tsx - Gbtc +// +// Copyright © 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 02/05/2026 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import { Modal, LoadingIcon, Alert } from '@gpa-gemstone/react-interactive'; +import { Application, Gemstone } from '@gpa-gemstone/application-typings'; +import { ReadOnlyControllerFunctions_Gemstone } from '@gpa-gemstone/common-pages'; +import { OpenSee } from '../../global'; +import { Column, Table } from '@gpa-gemstone/react-table'; + +interface Iprops { + Show: boolean, + SetShow: (show: boolean) => void, + EventID: number +} + +const FaultSpecificsController = new ReadOnlyControllerFunctions_Gemstone(`${homePath}api/openSEE/FaultSpecifics`); + +const FaultSpecificsModal = (props: Iprops) => { + const [faultSpecifics, setFaultSpecifics] = React.useState(null); + const [status, setStatus] = React.useState('uninitiated'); + + React.useEffect(() => { + setStatus('loading'); + + const handle = FaultSpecificsController.GetOne(props.EventID); + handle.then(obj => { + setFaultSpecifics(obj); + setStatus('idle'); + }, () => setStatus('error')); + + return () => { if (handle?.abort == null) handle.abort(); } + }, [props.EventID]); + + //It would be nice if we could get an endpoint on the ModelController that returned the label attributes for a models properties? + const faultTableData: Gemstone.TSX.Interfaces.ILabelValue[] = React.useMemo(() => { + if (faultSpecifics == null) return []; + + return Object.keys(faultSpecifics) + .filter((key) => key !== 'ID') + .map((key) => { + const typedKey = key as keyof OpenSee.IFaultSpecifics; + + return { + Label: getLabel(typedKey), + Value: String(faultSpecifics[typedKey]) + }; + }); + }, [faultSpecifics]) + + return ( + props.SetShow(false)} + Show={props.Show} + ShowConfirm={false} + CancelText='Close' + ShowX={true} + Size='lg' + > + + {status === 'error' ? +
    +
    + + Error retrieving fault information. + +
    +
    + : null} + {status === 'idle' ? +
    + > + Data={faultTableData} + SortKey="Label" + Ascending={true} + OnSort={() => {/*do nothing*/ }} + TableClass="table" + TbodyStyle={{ overflowY: 'auto', maxHeight: 500 }} + KeySelector={(item) => item.Label} + > + > + Key='Label' + Field="Label" + AllowSort={false} + > + {''} + + > + Key='Value' + Field='Value' + AllowSort={false} + > + {''} + + +
    + : null} +
    + ); +} + +const getLabel = (key: keyof OpenSee.IFaultSpecifics): string => { + switch (key) { + case 'ID': + return 'ID'; + + case 'FaultType': + return 'Fault Type'; + + case 'Inception': + return 'Inception'; + + case 'DurationMs': + return 'Duration (ms)'; + + case 'DurationCycles': + return 'Duration (Cycles)'; + + case 'DeltaTime': + return 'Delta Time'; + + case 'CurrentMagnitude': + return 'Current Magnitude'; + + case 'Algorithm': + return 'Algorithm'; + + case 'Distance': + return 'Distance'; + + case 'DoubleFaultDistance': + return 'Double Fault Distance'; + + case 'DoubleFaultAngle': + return 'Double Fault Angle'; + + case 'StartTime': + return 'Start Time'; + + case 'MeterName': + return 'Meter Name'; + + default: + return key; + } +}; + +export default FaultSpecificsModal diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/EventStats.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/EventStats.tsx new file mode 100644 index 00000000..25ce7046 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/EventStats.tsx @@ -0,0 +1,149 @@ +//****************************************************************************************************** +// ScalarStats.tsx - Gbtc +// +// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/14/2018 - Billy Ernest +// Generated original version of source code. +// +// 12/07/2023 - Preston Crawford +// Switched table elements to a gpa-gemstone component +//****************************************************************************************************** + +import * as React from 'react'; +import { Application } from '@gpa-gemstone/application-typings'; +import { Alert } from '@gpa-gemstone/react-interactive'; +import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; +import { Table, Column } from '@gpa-gemstone/react-table'; +import { CreateGuid } from '@gpa-gemstone/helper-functions'; + +interface IEventData { + Stat: string, + Value: string, + ID: string // created clientside for table +} + +interface Iprops { + EventID: number, + ExportCallback: (arg: string) => void +} + +const EventStatsWidget = (props: Iprops) => { + const [stats, setStats] = React.useState([]); + const [status, setStatus] = React.useState('uninitiated'); + + React.useEffect(() => { + setStatus('loading'); + + const handle = $.ajax({ + type: "GET", + url: `${homePath}api/OpenSEE/GetScalarStats?eventId=${props.EventID}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }); + handle.done((d) => { + const t: IEventData[] = [] + Object.keys(d).forEach(stat => { + t.push({ Stat: stat, Value: d[stat], ID: CreateGuid() }) + }) + setStats(t); + setStatus('idle'); + }); + handle.fail(() => setStatus('error')); + + return () => { if (handle != undefined && handle.abort != undefined) handle.abort(); } + }, [props.EventID]); + + return ( + <> +
    + {status === 'loading' || status === 'uninitiated' ? +
    + +
    + : null} + {status === 'error' ? +
    +
    + + Error retrieving event stats. + +
    +
    + : null} + {status === 'idle' && stats.length === 0 ? +
    +
    + + No data for event stats. + +
    +
    + : null} + {status === 'idle' && stats.length > 0 ? + <> +
    + +
    +
    + + TableClass="table table-hover w-100" + Data={stats} + SortKey={""} + Ascending={true} + OnSort={() => { }} + OnClick={() => { }} + Selected={() => false} + KeySelector={(item) => item.ID} + > + + Key={'Stat'} + AllowSort={false} + Field={'Stat'} + Content={({ item }) => getStatLabel(item.Stat)} + > + Stat + + + Key={'Value'} + AllowSort={false} + Field={'Value'} + > + Value + + +
    + + : null} +
    + + ); +} + +const getStatLabel = (stat: string) => { + switch (stat) { + case 'AssetKey': + return 'Asset Key'; + case 'AssetName': + return 'Asset Name'; + default: + return stat; + } +} + +export default EventStatsWidget; \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/FFTTable.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/FFTTable.tsx new file mode 100644 index 00000000..1b7101d2 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/FFTTable.tsx @@ -0,0 +1,113 @@ +//****************************************************************************************************** +// FFTTable.tsx - Gbtc +// +// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/14/2018 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** +import * as React from 'react'; +import { PlotDataStateContext } from '../Context/PlotDataContext'; +import { PlotStateStateContext } from '../Context/PlotStateContext'; +import EventContext from '../Context/EventContext'; +import { selectFFTData } from '../PlotSelectors'; +import { OpenSee } from '../global'; +import { Alert } from '@gpa-gemstone/react-interactive'; +import { useGetContainerPosition } from '@gpa-gemstone/helper-functions'; + +const FFTTable = () => { + const { plots } = React.useContext(PlotDataStateContext); + const { meta } = React.useContext(PlotStateStateContext); + const evt = React.useContext(EventContext); + + const fftPoints = React.useMemo(() => selectFFTData(evt.Context.EventID, plots, meta), [evt.Context.EventID, plots, meta]); + + const topRowRef = React.useRef(null); + const {offsetHeight: topRowHeight} = useGetContainerPosition(topRowRef); + + if (fftPoints.length === 0) + return ( +
    +
    + + No data for FFT Table. + +
    +
    + ); + + return ( +
    +
    + + + + + {fftPoints.map((item, index) => ( + + ))} + + + + {fftPoints.map((item, index) => ( + + + + + ))} + + + + {fftPoints[0].Angle.map((a, row) => ( + + + {fftPoints.map((_, index) => ( + + {showMag(index, row, fftPoints)} + {showAng(index, row, fftPoints)} + + ))} + + ))} + +
    {item.Asset} {item.Phase}
    Harmonic [Hz]Mag ({item?.Unit?.short})Ang ({item?.PhaseUnit?.short})
    {(row > 0 ? fftPoints[0].Frequency[row].toFixed(2) : 'DC')}
    +
    +
    + ); +}; + +const getStickyHeaderStyle = (top: number): React.CSSProperties => ({ + position: 'sticky', + top, + zIndex: 1, + backgroundColor: '#fff', + boxShadow: 'inset 0 -1px 0 #dee2e6' +}); + +const showAng = (index: number, row: number, fftPoints: OpenSee.IFFTSeries[]) => { + const f = fftPoints[index].PhaseUnit != undefined ? fftPoints[index].PhaseUnit.factor : 1.0; + const val = fftPoints[index].Angle[row] * (f ?? 1); + return isNaN(val) ? N/A : {val.toFixed(2)}; +}; + +const showMag = (index: number, row: number, fftPoints: OpenSee.IFFTSeries[]) => { + const f = (fftPoints?.[index]?.Unit?.factor === undefined ? 1.0 / fftPoints?.[index]?.BaseValue : fftPoints[index]?.Unit?.factor); + const val = fftPoints?.[index]?.Magnitude[row] * f; + return isNaN(val) ? N/A : {val.toFixed(2)}; +}; + +export default FFTTable; \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/HarmonicStats.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/HarmonicStats.tsx new file mode 100644 index 00000000..6bb2b28e --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/HarmonicStats.tsx @@ -0,0 +1,146 @@ +//****************************************************************************************************** +// HarmonicStats.tsx - Gbtc +// +// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/14/2018 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import { Application } from '@gpa-gemstone/application-typings'; +import { Alert } from '@gpa-gemstone/react-interactive'; +import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; + +interface Iprops { + EventID: number, + ExportCallback: (arg: string) => void +} + +const HarmonicStatsWidget = (props: Iprops) => { + const [tblData, setTblData] = React.useState>([]); + const [status, setStatus] = React.useState('uninitiated'); + + React.useEffect(() => { + setStatus('loading'); + + const handle = $.ajax({ + type: "GET", + url: `${homePath}api/OpenSEE/GetHarmonics?eventId=${props.EventID}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }); + + handle.done((data) => { + if (data.length === 0) { + setTblData([]); + setStatus('idle'); + return; + } + + const rows: JSX.Element[] = []; + rows.push( + + + {data.map((key, i) => {key.Channel})} + ) + + rows.push( + + Harmonic + {data.map((item, index) => Mag Ang )} + ) + + + const numChannels = data.length; + const jsons = data.map(x => JSON.parse(x.SpectralData)); + const numHarmonics = Math.max(...jsons.map(x => Object.keys(x).length)); + + for (let index = 1; index <= numHarmonics; ++index) { + const tds: JSX.Element[] = []; + const label = 'H' + index + for (let j = 0; j < numChannels; ++j) { + const key = data[j].Channel + label + if (jsons[j][label] != undefined) { + tds.push({jsons[j][label].Magnitude.toFixed(2)}); + tds.push({jsons[j][label].Angle.toFixed(2)}); + } + else { + tds.push(); + tds.push(); + } + } + rows.push( + + {label} + {tds} + ); + } + setTblData(rows); + setStatus('idle'); + }); + handle.fail(() => setStatus('error')); + + return () => { if (handle?.abort != null) handle.abort(); } + }, [props.EventID]) + + return ( + <> + {status === 'loading' || status === 'uninitiated' ? +
    + +
    + : null} + {status === 'error' ? +
    +
    + + Error retrieving harmonic stats. + +
    +
    + : null} + {status === 'idle' ? + tblData.length === 0 ? +
    +
    + + No data for Harmonic Stats. + +
    +
    + : +
    + + + {tblData[0]} + {tblData[1]} + + + {tblData.slice(2)} + +
    +
    + : null} + + ); + +} + +export default HarmonicStatsWidget; \ No newline at end of file diff --git a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/LightningData.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/LightningData.tsx similarity index 53% rename from src/OpenSEE/Scripts/TSX/jQueryUI Widgets/LightningData.tsx rename to src/OpenSEE/wwwroot/Scripts/TSX/Widgets/LightningData.tsx index 5830ffaa..afeeaaff 100644 --- a/src/OpenSEE/Scripts/TSX/jQueryUI Widgets/LightningData.tsx +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/LightningData.tsx @@ -22,9 +22,11 @@ //****************************************************************************************************** import * as React from 'react'; -import { ConfigTable } from '@gpa-gemstone/react-interactive'; -import { ReactTable } from '@gpa-gemstone/react-table' - +import { Application } from '@gpa-gemstone/application-typings'; +import { Alert } from '@gpa-gemstone/react-interactive'; +import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; +import { ConfigurableTable, ConfigurableColumn, Column } from '@gpa-gemstone/react-table' +import EventContext from '../Context/EventContext'; interface Column { key: string; @@ -59,14 +61,17 @@ interface LightningData { // these probably arent all strings but not sure since const LightningDataWidget = () => { - const [lightningData, setLightningData] = React.useState(null); - const [cols, setCols] = React.useState([]); + const evt = React.useContext(EventContext); + + const [lightningData, setLightningData] = React.useState([]); + const [status, setStatus] = React.useState('uninitiated'); - function getData(): JQuery.jqXHR { + React.useEffect(() => { + setStatus('loading'); const handle = $.ajax({ type: "GET", - url: `${homePath}api/OpenSEE/GetLightningData?eventID=${eventID}`, + url: `${homePath}api/OpenSEE/GetLightningData?eventID=${evt.Context.EventID}`, contentType: "application/json; charset=utf-8", dataType: 'json', cache: true, @@ -75,22 +80,32 @@ const LightningDataWidget = () => { handle.done(lightningData => { setLightningData(lightningData) + setStatus('idle'); }); - - return handle; - } - - React.useEffect(() => { - const handle = getData(); + handle.fail(() => setStatus('error')); return () => { if (handle !== undefined && handle.abort !== undefined) handle.abort(); } - }, [eventID]); + }, [evt.Context.EventID]); return ( <> - {lightningData ? + {status === 'loading' || status === 'uninitiated' ? +
    + +
    + : null} + {status === 'error' ? +
    +
    + + Error retrieving lightning data. + +
    +
    + : null} + {status === 'idle' && lightningData.length > 0 ?
    - + LocalStorageKey={"OpenSee.Lightning.TableCols"} TableClass={"table table-hover"} Data={lightningData} @@ -102,210 +117,218 @@ const LightningDataWidget = () => { TableStyle={{ height: '100%', width: '100%', margin: '3%' }} Ascending={false} > - - + + Key={'Service'} AllowSort={true} Field={'Service'}> Service - - + + - - + + Key={'UTCTime'} AllowSort={true} Field={'UTCTime'}> UTC Time - - + + - - + + Key={'DisplayTime'} AllowSort={true} Field={'DisplayTime'}> Display Time - - + + - - + + Key={'Amplitude'} AllowSort={true} Field={'Amplitude'}> Amplitude - - + + - - + + Key={'Latitude'} AllowSort={true} Field={'Latitude'}> Latitude - - + + - - + + Key={'Longitude'} AllowSort={true} Field={'Longitude'}> Longitude - - + + - - + + Key={'PeakCurrent'} AllowSort={true} Field={'PeakCurrent'}> Peak Current - - + + - - + + Key={'FlashMultiplicity'} AllowSort={true} Field={'FlashMultiplicity'}> Flash Multiplicity - - + + - - + + Key={'ParticipatingSensors'} AllowSort={true} Field={'ParticipatingSensors'}> Participating Sensors - - + + - - + + Key={'DegreesOfFreedom'} AllowSort={true} Field={'DegreesOfFreedom'}> Degrees Of Freedom - - + + - - + + Key={'EllipseAngle'} AllowSort={true} Field={'EllipseAngle'}> Ellipse Angle - - + + - - + + Key={'SemiMajorAxisLength'} AllowSort={true} Field={'SemiMajorAxisLength'}> Semi Major Axis Length - - + + - - + + Key={'SemiMinorAxisLength'} AllowSort={true} Field={'SemiMinorAxisLength'}> Semi Minor Axis Length - - + + - - + + Key={'ChiSquared'} AllowSort={true} Field={'ChiSquared'}> Chi Squared - - + + - - + + Key={'Risetime'} AllowSort={true} Field={'Risetime'}> Rise time - - + + - - + + Key={'FlashMultiplicity'} AllowSort={true} Field={'FlashMultiplicity'}> Flash Multiplicity - - + + - - + + Key={'PeakToZeroTime'} AllowSort={true} Field={'PeakToZeroTime'}> Peak To Zero Time - - + + - - + + Key={'MaximumRateOfRise'} AllowSort={true} Field={'MaximumRateOfRise'}> Maximum Rate Of Rise - - + + - - + + Key={'CloudIndicator'} AllowSort={true} Field={'CloudIndicator'}> Cloud Indicator - - + + - - + + Key={'AngleIndicator'} AllowSort={true} Field={'AngleIndicator'}> Angle Indicator - - + + - - + + Key={'SignalIndicator'} AllowSort={true} Field={'SignalIndicator'}> Signal Indicator - - + + - - + + Key={'TimingIndicator'} AllowSort={true} Field={'TimingIndicator'}> Timing Indicator - - - + + +
    + : null} + {status === 'idle' && lightningData.length === 0 ? +
    +
    + + No lightning data found. + +
    +
    : null} ); } -export default LightningDataWidget; - +export default LightningDataWidget; \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PhasorChart.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PhasorChart.tsx new file mode 100644 index 00000000..9d9f8e80 --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PhasorChart.tsx @@ -0,0 +1,160 @@ +//****************************************************************************************************** +// PolarChart.tsx - Gbtc +// +// Copyright © 2018, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/10/2018 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** +import * as _ from 'lodash'; +import * as React from 'react'; +import { useSelector } from 'react-redux'; +import { PlotDataStateContext } from '../Context/PlotDataContext'; +import { PlotStateStateContext } from '../Context/PlotStateContext'; +import EventContext from '../Context/EventContext'; +import HoverContext from '../Context/HoverContext'; +import { OpenSee } from '../global'; +import { SelectColor } from '../Store/settingSlice'; +import { selectPhaseVectors } from '../PlotSelectors'; +import { useGetContainerPosition } from '@gpa-gemstone/helper-functions'; +import { Alert } from '@gpa-gemstone/react-interactive'; + +const PhasorChartWidget = () => { + const [hover] = React.useContext(HoverContext); + const { plots } = React.useContext(PlotDataStateContext); + const { meta } = React.useContext(PlotStateStateContext); + const evt = React.useContext(EventContext); + const colors = useSelector(SelectColor); + + const VVector = React.useMemo( + () => selectPhaseVectors(hover, evt.Context.EventID, 'Voltage', plots, meta), + [hover, evt.Context.EventID, plots, meta] + ); + const IVector = React.useMemo( + () => selectPhaseVectors(hover, evt.Context.EventID, 'Current', plots, meta), + [hover, evt.Context.EventID, plots, meta] + ); + + const [AssetList, setAssetList] = React.useState([]); + const [scaleV, setScaleV] = React.useState(0); + const [scaleI, setScaleI] = React.useState(0); + + const svgRef = React.useRef(null); + const { clientWidth, clientHeight } = useGetContainerPosition(svgRef); + + React.useEffect(() => { + const timeoutId = setTimeout(() => { + const newAssetList = _.uniq([...VVector.map(item => item.Asset), ...IVector.map(item => item.Asset)]); + if (!_.isEqual(newAssetList.sort(), AssetList.sort())) { + setAssetList(newAssetList); + } + setScaleV(0.9 * Math.max(clientWidth / 2, clientHeight / 2) / Math.max(...VVector.map(item => item.Magnitude))); + setScaleI(0.9 * Math.max(clientWidth / 2, clientHeight / 2) / Math.max(...IVector.map(item => item.Magnitude))); + }, 100); + + return () => clearTimeout(timeoutId); + }, [VVector, IVector]); + + function drawVectorSVG(vec: OpenSee.IVector, scale: number) { + if (vec.Magnitude === undefined || scale === undefined) return ''; + const centerX = clientWidth / 2; + const centerY = clientHeight / 2; + const x = vec.Magnitude * scale * Math.cos(vec.Angle * Math.PI / 180); + const y = vec.Magnitude * scale * Math.sin(vec.Angle * Math.PI / 180); + return `M ${centerX} ${centerY} L ${centerX + x} ${centerY - y} Z`; + } + + function createTable(vec: OpenSee.IVector | undefined, index: number) { + if (vec == undefined) + return N/AN/A; + + const factor = (vec.Unit.factor === undefined ? (1.0 / vec.BaseValue) : vec.Unit.factor); + const phaseFactor = (vec.PhaseUnit.factor === undefined ? (1.0 / vec.BaseValue) : vec.PhaseUnit.factor); + + return ( + + {(vec.Magnitude * factor).toFixed(2)} {vec.Unit.short} + {(vec.Angle * phaseFactor).toFixed(2)} {vec.PhaseUnit.short} + + ); + } + + if (VVector.length === 0 && IVector.length === 0) + return ( +
    +
    + + No data for Phasor Chart. + +
    +
    + ); + + return ( +
    +
    + + {AssetList.map((asset, ai) => ( + + {VVector.filter(v => v.Asset === asset).map((v, vi) => ( + + ))} + {IVector.filter(v => v.Asset === asset).map((v, vi) => ( + + ))} + + ))} + +
    +
    + {AssetList.map((asset, ai) => { + const vPhases = _.uniq(VVector.filter(v => v.Asset === asset).map(v => v.Phase)); + const iPhases = _.uniq(IVector.filter(v => v.Asset === asset).map(v => v.Phase)); + const allPhases = _.uniq([...vPhases, ...iPhases]); + + return ( + + + + + + + + + + + + + + + {allPhases.map((phase, pi) => ( + + + {createTable(VVector.find(v => v.Asset === asset && v.Phase === phase), pi * 2)} + {createTable(IVector.find(v => v.Asset === asset && v.Phase === phase), pi * 2 + 1)} + + ))} + +
    {asset}VI
    PhaseMagAngMagAng
    {phase}
    + ); + })} +
    +
    + ); +}; + +export default PhasorChartWidget; \ No newline at end of file diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/AxisUnitSelector.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/AxisUnitSelector.tsx new file mode 100644 index 00000000..b331262c --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/AxisUnitSelector.tsx @@ -0,0 +1,62 @@ +//****************************************************************************************************** +// AxisUnitSelector.tsx - Gbtc +// +// Copyright c 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code. +// +//****************************************************************************************************** +import * as React from 'react'; +import { OpenSee } from '../../global'; +import { defaultSettings } from '../../defaults'; +import { Select } from '@gpa-gemstone/react-forms'; + +interface IProps { + setter: (index: number) => void, + unitType: OpenSee.Unit, + axisSetting: OpenSee.IAxisSettings +} + +//probably doesnt have to be memoized here +const AxisUnitSelector = React.memo((props: IProps) => { + const { current, isAuto } = props.axisSetting; + + const options = React.useMemo(() => { + const unit: OpenSee.IUnitSetting = defaultSettings.Units[props.unitType]; + + if(unit == null || unit?.options == null) + return [] + + return unit.options.map((option, index) => ({ + Label: isAuto && current === index && option.factor !== 0 ? + `${option.label} (auto)` : option.label, + Value: index + })) + }, [props.unitType, current, isAuto]); + + return ( + handleTimeChange(start.startMS, true)} + Field={"startMS"} + Valid={() => true} + Label={"Start"} + Type={"number"} + /> + +
    + handleTimeChange(end.endMS, false)} + Field={"endMS"} + Valid={() => true} + Label={"End"} + Type={"number"} + /> +
    + : +
    +
    + + Record={formattedTime} + Format={"HH:mm:ss.SSS"} + Field={'start'} + Setter={(e) => handleDateChange(e.start, true)} + Label={"Start Time"} + Accuracy={'millisecond'} + Valid={() => valid} + Type={'time'} + Feedback={"Start Time can not be greater than End Time"} + /> +
    +
    + + Record={formattedTime} + Format={"HH:mm:ss.SSS"} + Field={'end'} + Setter={(e) => handleDateChange(e.end, false)} + Label={"End Time"} + Valid={() => valid} + Type={'time'} + Accuracy={'millisecond'} + Feedback={"Start Time can not be greater than End Time"} + /> +
    +
    + } + +
    + Plot Markers: +
    +
    + dispatch(SetPlotMarkers(item.plotMarkers))} + Label={"Inception and Duration"} + Help={"For events without this information record start and end time will be used."} + /> +
    +
    +
    + + + {plotKeys + .filter(key => key.EventId === evt.Context.EventID || key.EventId === -1) + .map((item, index) => + + )} + + + + ); +}; + +export default SettingsWidget; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/TimeUnitSelector.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/TimeUnitSelector.tsx new file mode 100644 index 00000000..d91bae2f --- /dev/null +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/TimeUnitSelector.tsx @@ -0,0 +1,52 @@ +//****************************************************************************************************** +// AxisUnitSelector.tsx - Gbtc +// +// Copyright c 2026, 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 MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 05/08/2026 - Preston Crawford +// Generated original version of source code. +// +//****************************************************************************************************** +import * as React from 'react'; +import { defaultSettings } from '../../defaults'; +import { Select } from '@gpa-gemstone/react-forms'; + +interface IProps { + setter: (index: number) => void, + timeUnitIndex: number, + overlappingWave?: boolean +} + +const TimeUnitSelector = React.memo((props: IProps) => { + const options = React.useMemo(() => { + const src = props.overlappingWave + ? (defaultSettings.OverlappingWaveTimeUnit.options ?? []) + : (defaultSettings.TimeUnit.options ?? []); + return src.map((option, index) => ({ Label: option.label, Value: index })); + }, [props.overlappingWave]); + + return ( +