diff --git a/src/lib/Common.cs b/src/lib/sttp.core/Common.cs similarity index 88% rename from src/lib/Common.cs rename to src/lib/sttp.core/Common.cs index 2a0972df..c211ff63 100644 --- a/src/lib/Common.cs +++ b/src/lib/sttp.core/Common.cs @@ -23,8 +23,9 @@ // //****************************************************************************************************** -using System.Security.Cryptography; -using GSF.Threading; +#if NET +#define MONO +#endif #if !MONO using Microsoft.Win32; @@ -40,13 +41,13 @@ public static class Common static Common() { #if MONO - UseManagedEncryption = true; + UseManagedEncryption = true; #else const string FipsKeyOld = @"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa"; const string FipsKeyNew = @"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy"; // Determine if the operating system configuration to set to use FIPS-compliant algorithms - UseManagedEncryption = (Registry.GetValue(FipsKeyNew, "Enabled", 0) ?? Registry.GetValue(FipsKeyOld, "FipsAlgorithmPolicy", 0)).ToString() == "0"; + UseManagedEncryption = ((Registry.GetValue(FipsKeyNew, "Enabled", 0) ?? Registry.GetValue(FipsKeyOld, "FipsAlgorithmPolicy", 0))?.ToString() ?? "0") == "0"; #endif TimerScheduler = new SharedTimerScheduler(); @@ -64,7 +65,12 @@ public static SymmetricAlgorithm SymmetricAlgorithm { get { + #if NET + Aes symmetricAlgorithm = Aes.Create(); + #else Aes symmetricAlgorithm = UseManagedEncryption ? new AesManaged() : new AesCryptoServiceProvider(); + #endif + symmetricAlgorithm.KeySize = 256; return symmetricAlgorithm; } diff --git a/src/lib/CompactMeasurement.cs b/src/lib/sttp.core/CompactMeasurement.cs similarity index 98% rename from src/lib/CompactMeasurement.cs rename to src/lib/sttp.core/CompactMeasurement.cs index b1a59b9e..6b7f118e 100644 --- a/src/lib/CompactMeasurement.cs +++ b/src/lib/sttp.core/CompactMeasurement.cs @@ -27,12 +27,6 @@ // //****************************************************************************************************** -using System; -using GSF; -using GSF.Parsing; -using GSF.TimeSeries; -using GSF.TimeSeries.Transport; - namespace sttp; #region [ Enumerations ] @@ -195,7 +189,7 @@ public class CompactMeasurement : Measurement, IBinaryMeasurement /// Base time offset array - set to null to use full fidelity measurement time. /// Time index to use for base offset. /// Flag that determines if millisecond resolution is in use for this serialization. - public CompactMeasurement(SignalIndexCache signalIndexCache, bool includeTime = true, long[] baseTimeOffsets = null, int timeIndex = 0, bool useMillisecondResolution = false) + public CompactMeasurement(SignalIndexCache signalIndexCache, bool includeTime = true, long[]? baseTimeOffsets = null, int timeIndex = 0, bool useMillisecondResolution = false) { m_signalIndexCache = signalIndexCache; IncludeTime = includeTime; @@ -215,7 +209,7 @@ public CompactMeasurement(SignalIndexCache signalIndexCache, bool includeTime = /// Base time offset array - set to null to use full fidelity measurement time. /// Time index to use for base offset. /// Flag that determines if millisecond resolution is in use for this serialization. - public CompactMeasurement(IMeasurement measurement, SignalIndexCache signalIndexCache, bool includeTime = true, long[] baseTimeOffsets = null, int timeIndex = 0, bool useMillisecondResolution = false) + public CompactMeasurement(IMeasurement measurement, SignalIndexCache signalIndexCache, bool includeTime = true, long[]? baseTimeOffsets = null, int timeIndex = 0, bool useMillisecondResolution = false) { Metadata = measurement.Metadata; Value = measurement.Value; @@ -335,7 +329,7 @@ public int RuntimeID set { // Attempt to restore signal identification - if (m_signalIndexCache.Reference.TryGetValue(value, out MeasurementKey key)) + if (m_signalIndexCache.Reference.TryGetValue(value, out MeasurementKey? key) /* && key is not null */) Metadata = key.Metadata; else throw new InvalidOperationException($"Failed to find associated signal identification for runtime ID {value}"); diff --git a/src/lib/DataGapRecoverer.cs b/src/lib/sttp.core/DataGapRecoverer.cs similarity index 93% rename from src/lib/DataGapRecoverer.cs rename to src/lib/sttp.core/DataGapRecoverer.cs index 30538b2d..d8957f99 100644 --- a/src/lib/DataGapRecoverer.cs +++ b/src/lib/sttp.core/DataGapRecoverer.cs @@ -21,21 +21,11 @@ // //****************************************************************************************************** -using System; -using System.Collections.Generic; -using System.Data; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using GSF; -using GSF.Diagnostics; -using GSF.IO; -using GSF.Threading; -using GSF.TimeSeries; -using GSF.TimeSeries.Adapters; -using GSF.Units; +#if NET +using CommonFunc = Gemstone.Common; +#else +using CommonFunc = GSF.Common; +#endif namespace sttp; @@ -110,7 +100,7 @@ public class DataGapRecoverer : ISupportLifecycle, IProvideStatus /// /// is a collection of measurements for consumer to process. /// - public event EventHandler>> RecoveredMeasurements; + public event EventHandler>>? RecoveredMeasurements; /// /// Provides status messages to consumer. @@ -118,7 +108,7 @@ public class DataGapRecoverer : ISupportLifecycle, IProvideStatus /// /// is new status message. /// - public event EventHandler> StatusMessage; + public event EventHandler>? StatusMessage; /// /// Event is raised when there is an exception encountered during processing. @@ -126,26 +116,26 @@ public class DataGapRecoverer : ISupportLifecycle, IProvideStatus /// /// is the exception that was thrown. /// - public event EventHandler> ProcessException; + public event EventHandler>? ProcessException; /// /// Raised after the has been properly disposed. /// - public event EventHandler Disposed; + public event EventHandler? Disposed; // Fields private readonly SubscriptionInfo m_subscriptionInfo; private readonly ManualResetEventSlim m_dataGapRecoveryCompleted; - private DataSubscriber m_temporalSubscription; - private SharedTimer m_dataStreamMonitor; - private DataSet m_dataSource; - private string m_loggingPath; - private string m_sourceConnectionName; - private string m_connectionString; + private readonly SharedTimer m_dataStreamMonitor; + private DataSubscriber? m_temporalSubscription; + private DataSet? m_dataSource; + private string m_loggingPath = string.Empty; + private string m_sourceConnectionName = default!; + private string m_connectionString = default!; private Time m_recoveryStartDelay; private Time m_minimumRecoverySpan; private Time m_maximumRecoverySpan; - private Outage m_currentDataGap; + private Outage? m_currentDataGap; private Ticks m_mostRecentRecoveredTime; private long m_measurementsRecoveredForDataGap; private long m_measurementsRecoveredOverLastInterval; @@ -230,7 +220,7 @@ public string SourceConnectionName /// /// Gets or sets based data source available to this . /// - public DataSet DataSource + public DataSet? DataSource { get => m_dataSource; set @@ -437,7 +427,7 @@ public bool UseMillisecondResolution /// Gets or sets any additional constraint parameters that will be supplied to adapters in temporal /// subscription used when recovering data for an . /// - public string ConstraintParameters + public string? ConstraintParameters { get => m_subscriptionInfo.ConstraintParameters; set => m_subscriptionInfo.ConstraintParameters = value; @@ -469,12 +459,12 @@ public bool Enabled /// /// Gets reference to the data gap for this . /// - protected OutageLog DataGapLog { get; private set; } + protected OutageLog? DataGapLog { get; private set; } /// /// Gets reference to the data gap for this . /// - protected OutageLogProcessor DataGapLogProcessor { get; private set; } + protected OutageLogProcessor? DataGapLogProcessor { get; private set; } // Gets the name of the data gap recoverer. string IProvideStatus.Name => m_temporalSubscription is null ? GetType().Name : m_temporalSubscription.Name; @@ -505,7 +495,7 @@ public string Status status.AppendLine($" Logging path: {FilePath.TrimFileName(m_loggingPath.ToNonNullNorWhiteSpace(FilePath.GetAbsolutePath("")), 51)}"); status.AppendLine($"Last recovered measurement: {((DateTime)m_mostRecentRecoveredTime).ToString(OutageLog.DateTimeFormat)}"); - Outage currentDataGap = m_currentDataGap; + Outage? currentDataGap = m_currentDataGap; if (currentDataGap is not null) status.AppendLine($" Currently recovering: {currentDataGap.Start.ToString(OutageLog.DateTimeFormat)} to {currentDataGap.End.ToString(OutageLog.DateTimeFormat)}"); @@ -557,20 +547,13 @@ protected virtual void Dispose(bool disposing) if (!disposing) return; - if (m_dataGapRecoveryCompleted is not null) - { - // Signal any waiting threads - m_abnormalTermination = true; - m_dataGapRecoveryCompleted.Set(); - m_dataGapRecoveryCompleted.Dispose(); - } + // Signal any waiting threads + m_abnormalTermination = true; + m_dataGapRecoveryCompleted.Set(); + m_dataGapRecoveryCompleted.Dispose(); - if (m_dataStreamMonitor is not null) - { - m_dataStreamMonitor.Elapsed -= DataStreamMonitor_Elapsed; - m_dataStreamMonitor.Dispose(); - m_dataStreamMonitor = null; - } + m_dataStreamMonitor.Elapsed -= DataStreamMonitor_Elapsed; + m_dataStreamMonitor.Dispose(); if (DataGapLogProcessor is not null) { @@ -613,7 +596,7 @@ public void Initialize() Dictionary settings = m_connectionString.ToNonNullString().ParseKeyValuePairs(); - if (settings.TryGetValue("sourceConnectionName", out string setting) && !string.IsNullOrWhiteSpace(setting)) + if (settings.TryGetValue("sourceConnectionName", out string? setting) && !string.IsNullOrWhiteSpace(setting)) m_sourceConnectionName = setting; if (settings.TryGetValue("recoveryStartDelay", out setting) && double.TryParse(setting, out double timeInterval)) @@ -683,7 +666,7 @@ public void Initialize() DataGapLog.Initialize(); // Setup data gap processor to process items one at a time, a 5-second minimum period is established between each gap processing - DataGapLogProcessor = new OutageLogProcessor(DataGapLog, ProcessDataGap, CanProcessDataGap, ex => OnProcessException(MessageLevel.Warning, ex), GSF.Common.Max(5000, (int)(m_recoveryStartDelay * 1000.0D))); + DataGapLogProcessor = new OutageLogProcessor(DataGapLog, ProcessDataGap, CanProcessDataGap, ex => OnProcessException(MessageLevel.Warning, ex), CommonFunc.Max(5000, (int)(m_recoveryStartDelay * 1000.0D))); } /// @@ -760,6 +743,9 @@ public bool RemoveDataGap(DateTimeOffset startTime, DateTimeOffset endTime) /// The contents of the outage log. public string DumpOutageLog() { + if (DataGapLog is null) + throw new InvalidOperationException("Data gap log is not defined -- cannot dump outage log."); + List outages = DataGapLog.Outages; StringBuilder dump = new(); @@ -781,6 +767,12 @@ private bool CanProcessDataGap(Outage dataGap) // to requeue the data gap outage so it will be processed again (could be that remote system is offline). private void ProcessDataGap(Outage dataGap) { + if (m_temporalSubscription is null) + throw new InvalidOperationException("Temporal subscription is not established -- cannot process data gap."); + + if (DataGapLog is null) + throw new InvalidOperationException("Data gap log is not defined -- cannot process data gap."); + // Establish start and stop time for temporal session m_subscriptionInfo.StartTime = dataGap.Start.ToString(OutageLog.DateTimeFormat, CultureInfo.InvariantCulture); m_subscriptionInfo.StopTime = dataGap.End.ToString(OutageLog.DateTimeFormat, CultureInfo.InvariantCulture); @@ -817,7 +809,7 @@ private void ProcessDataGap(Outage dataGap) if (m_abnormalTermination) { // Make sure any data recovered so far doesn't get unnecessarily re-recovered, this requires that source historian report data in time-sorted order - dataGap = new Outage(new DateTime(GSF.Common.Max((Ticks)dataGap.Start.Ticks, m_mostRecentRecoveredTime - (m_subscriptionInfo.UseMillisecondResolution ? Ticks.PerMillisecond : 1L)), DateTimeKind.Utc), dataGap.End); + dataGap = new Outage(new DateTime(CommonFunc.Max((Ticks)dataGap.Start.Ticks, m_mostRecentRecoveredTime - (m_subscriptionInfo.UseMillisecondResolution ? Ticks.PerMillisecond : 1L)), DateTimeKind.Utc), dataGap.End); // Re-insert adjusted data gap at the top of the processing queue DataGapLog.Add(dataGap); @@ -843,7 +835,7 @@ protected virtual void OnRecoveredMeasurements(ICollection measure { try { - RecoveredMeasurements?.Invoke(this, new EventArgs>(measurements)); + RecoveredMeasurements?.SafeInvoke(this, new EventArgs>(measurements)); } catch (Exception ex) { @@ -864,14 +856,14 @@ protected virtual void OnRecoveredMeasurements(ICollection measure /// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this /// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred. /// - protected virtual void OnStatusMessage(MessageLevel level, string status, string eventName = null, MessageFlags flags = MessageFlags.None) + protected virtual void OnStatusMessage(MessageLevel level, string status, string? eventName = null, MessageFlags flags = MessageFlags.None) { try { Log.Publish(level, flags, eventName ?? "DataGapRecovery", status); using (Logger.SuppressLogMessages()) - StatusMessage?.Invoke(this, new EventArgs(AdapterBase.GetStatusWithMessageLevelPrefix(status, level))); + StatusMessage?.SafeInvoke(this, new EventArgs(AdapterBase.GetStatusWithMessageLevelPrefix(status, level))); } catch (Exception ex) { @@ -892,14 +884,14 @@ protected virtual void OnStatusMessage(MessageLevel level, string status, string /// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this /// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred. /// - protected virtual void OnProcessException(MessageLevel level, Exception exception, string eventName = null, MessageFlags flags = MessageFlags.None) + protected virtual void OnProcessException(MessageLevel level, Exception exception, string? eventName = null, MessageFlags flags = MessageFlags.None) { try { - Log.Publish(level, flags, eventName ?? "DataGapRecovery", exception?.Message, null, exception); + Log.Publish(level, flags, eventName ?? "DataGapRecovery", exception.Message, null, exception); using (Logger.SuppressLogMessages()) - ProcessException?.Invoke(this, new EventArgs(exception)); + ProcessException?.SafeInvoke(this, new EventArgs(exception)); } catch (Exception ex) { @@ -913,12 +905,12 @@ private string GetLoggingPath(string filePath) return string.IsNullOrWhiteSpace(m_loggingPath) ? FilePath.GetAbsolutePath(filePath) : Path.Combine(m_loggingPath, filePath); } - private void TemporalSubscription_ConnectionEstablished(object sender, EventArgs e) + private void TemporalSubscription_ConnectionEstablished(object? sender, EventArgs e) { m_connected = true; } - private void TemporalSubscription_ConnectionTerminated(object sender, EventArgs e) + private void TemporalSubscription_ConnectionTerminated(object? sender, EventArgs e) { m_connected = false; @@ -937,7 +929,7 @@ private void TemporalSubscription_ConnectionTerminated(object sender, EventArgs } } - private void TemporalSubscription_ProcessingComplete(object sender, EventArgs e) + private void TemporalSubscription_ProcessingComplete(object? sender, EventArgs e) { OnStatusMessage(MessageLevel.Info, "Temporal data recovery processing completed."); @@ -945,7 +937,7 @@ private void TemporalSubscription_ProcessingComplete(object sender, EventArgs> e) + private void TemporalSubscription_NewMeasurements(object? sender, EventArgs> e) { ICollection measurements = e.Argument; int total = measurements.Count; @@ -976,17 +968,17 @@ private void TemporalSubscription_NewMeasurements(object sender, EventArgs e) + private void Common_StatusMessage(object? sender, EventArgs e) { OnStatusMessage(MessageLevel.Info, e.Argument); } - private void Common_ProcessException(object sender, EventArgs e) + private void Common_ProcessException(object? sender, EventArgs e) { OnProcessException(MessageLevel.Warning, e.Argument); } - private void DataStreamMonitor_Elapsed(object sender, EventArgs e) + private void DataStreamMonitor_Elapsed(object? sender, EventArgs e) { if (m_measurementsRecoveredOverLastInterval == 0) { diff --git a/src/lib/DataPublisher.cs b/src/lib/sttp.core/DataPublisher.cs similarity index 86% rename from src/lib/DataPublisher.cs rename to src/lib/sttp.core/DataPublisher.cs index 96afed43..4c9e9a44 100644 --- a/src/lib/DataPublisher.cs +++ b/src/lib/sttp.core/DataPublisher.cs @@ -32,40 +32,8 @@ // Modified Header. // //****************************************************************************************************** - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.ComponentModel; -using System.Configuration; -using System.Data; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Xml; -using GSF; -using GSF.Communication; -using GSF.Configuration; -using GSF.Data; -using GSF.Diagnostics; -using GSF.IO; -using GSF.Net.Security; -using GSF.Security.Cryptography; -using GSF.Threading; -using GSF.TimeSeries; -using GSF.TimeSeries.Adapters; -using GSF.TimeSeries.Statistics; -using GSF.TimeSeries.Transport; -using GSF.Units; -using TcpClient = GSF.Communication.TcpClient; +// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident +// ReSharper disable UseUtf8StringLiteral namespace sttp; @@ -473,6 +441,7 @@ public enum DataPacketFlags : byte /// Bit set = compact, bit clear = full fidelity. /// Compact = (byte)Bits.Bit01, + /// /// Determines which cipher index to use when encrypting data packet. /// @@ -480,6 +449,7 @@ public enum DataPacketFlags : byte /// Bit set = use odd cipher index (i.e., 1), bit clear = use even cipher index (i.e., 0). /// CipherIndex = (byte)Bits.Bit02, + /// /// Determines if data packet payload is compressed. /// @@ -487,6 +457,7 @@ public enum DataPacketFlags : byte /// Bit set = payload compressed, bit clear = payload normal. /// Compressed = (byte)Bits.Bit03, + /// /// Determines which Signal Index Cache index to use when decoding a data packet. /// @@ -494,6 +465,7 @@ public enum DataPacketFlags : byte /// Bit set = use odd Signal Index Cache index (i.e., 1), bit clear = use even Signal Index Cache index (i.e., 0). /// CacheIndex = (byte)Bits.Bit04, + /// /// No flags set. /// @@ -522,6 +494,7 @@ public enum OperationalModes : uint /// Version number is currently set to 2. /// VersionMask = (uint)(Bits.Bit04 | Bits.Bit03 | Bits.Bit02 | Bits.Bit01 | Bits.Bit00), + /// /// Mask to get mode of compression. /// @@ -530,6 +503,7 @@ public enum OperationalModes : uint /// reserved for future compression modes. /// CompressionModeMask = (uint)(Bits.Bit07 | Bits.Bit06 | Bits.Bit05), + /// /// Mask to get character encoding used when exchanging messages between publisher and subscriber. /// @@ -540,6 +514,7 @@ public enum OperationalModes : uint /// 11 = ANSI /// EncodingMask = (uint)(Bits.Bit09 | Bits.Bit08), + /// /// Determines whether external measurements are exchanged during metadata synchronization. /// @@ -547,6 +522,7 @@ public enum OperationalModes : uint /// Bit set = external measurements are exchanged, bit clear = no external measurements are exchanged /// ReceiveExternalMetadata = (uint)Bits.Bit25, + /// /// Determines whether internal measurements are exchanged during metadata synchronization. /// @@ -554,6 +530,7 @@ public enum OperationalModes : uint /// Bit set = internal measurements are exchanged, bit clear = no internal measurements are exchanged /// ReceiveInternalMetadata = (uint)Bits.Bit26, + /// /// Determines whether payload data is compressed when exchanging between publisher and subscriber. /// @@ -561,6 +538,7 @@ public enum OperationalModes : uint /// Bit set = compress, bit clear = no compression /// CompressPayloadData = (uint)Bits.Bit29, + /// /// Determines whether the signal index cache is compressed when exchanging between publisher and subscriber. /// @@ -568,6 +546,7 @@ public enum OperationalModes : uint /// Bit set = compress, bit clear = no compression /// CompressSignalIndexCache = (uint)Bits.Bit30, + /// /// Determines whether metadata is compressed when exchanging between publisher and subscriber. /// @@ -575,6 +554,7 @@ public enum OperationalModes : uint /// Bit set = compress, bit clear = no compression /// CompressMetadata = (uint)Bits.Bit31, + /// /// No flags set. /// @@ -595,10 +575,12 @@ public enum OperationalEncoding : uint /// UTF-16, little endian /// UTF16LE = (uint)Bits.Nil, + /// /// UTF-16, big endian /// UTF16BE = (uint)Bits.Bit08, + /// /// UTF-8 /// @@ -615,10 +597,12 @@ public enum CompressionModes : uint /// GZip compression /// GZip = (uint)Bits.Bit05, + /// /// TSSC compression /// TSSC = (uint)Bits.Bit06, + /// /// No compression /// @@ -645,7 +629,7 @@ public LatestMeasurementCache(string connectionString) ConnectionString = connectionString; } - public override DataSet DataSource + public override DataSet? DataSource { get => base.DataSource; set @@ -672,7 +656,7 @@ public override string GetShortStatus(int maxLength) private void UpdateInputMeasurementKeys() { - if (Settings.TryGetValue("inputMeasurementKeys", out string inputMeasurementKeys)) + if (Settings.TryGetValue("inputMeasurementKeys", out string? inputMeasurementKeys)) InputMeasurementKeys = ParseInputMeasurementKeys(DataSource, true, inputMeasurementKeys); } } @@ -687,7 +671,7 @@ private void UpdateInputMeasurementKeys() /// is the connection identification (e.g., IP and DNS name, if available).
/// is the subscriber information as reported by the client. /// - public event EventHandler> ClientConnected; + public event EventHandler>? ClientConnected; /// /// Indicates to the host that processing for an input adapter (via temporal session) has completed. @@ -696,7 +680,7 @@ private void UpdateInputMeasurementKeys() /// This event is expected to only be raised when an input adapter has been designed to process /// a finite amount of data, e.g., reading a historical range of data during temporal processing. /// - public event EventHandler ProcessingComplete; + public event EventHandler? ProcessingComplete; // Constants @@ -755,19 +739,28 @@ private void UpdateInputMeasurementKeys() /// public const bool DefaultValidateClientIPAddress = false; +#if !NET /// /// Default value for . /// public const bool DefaultUseSimpleTcpClient = false; +#endif /// /// Default value for . /// public const string DefaultMetadataTables = + #if NET + "SELECT UniqueID, OriginalSource, IsConcentrator, Acronym, Name, AccessID, ParentAcronym, CompanyAcronym, VendorAcronym, VendorDeviceName, Longitude, Latitude, InterconnectionName, ContactList, Enabled, UpdatedOn FROM DeviceDetail WHERE IsConcentrator = 0;" + + "SELECT DeviceAcronym, ID, SignalID, PointTag, AlternateTag, SignalReference, SignalAcronym, PhasorSourceIndex, Description, Internal, Enabled, UpdatedOn FROM MeasurementDetail;" + + "SELECT ID, DeviceAcronym, Label, Type, Phase, PrimaryVoltageID, SecondaryVoltageID, SourceIndex, BaseKV, UpdatedOn FROM PhasorDetail;" + + "SELECT TOP 1 Version AS VersionNumber FROM VersionInfo AS SchemaVersion"; + #else "SELECT NodeID, UniqueID, OriginalSource, IsConcentrator, Acronym, Name, AccessID, ParentAcronym, ProtocolName, FramesPerSecond, CompanyAcronym, VendorAcronym, VendorDeviceName, Longitude, Latitude, InterconnectionName, ContactList, Enabled, UpdatedOn FROM DeviceDetail WHERE IsConcentrator = 0;" + "SELECT DeviceAcronym, ID, SignalID, PointTag, AlternateTag, SignalReference, SignalAcronym, PhasorSourceIndex, Description, Internal, Enabled, UpdatedOn FROM MeasurementDetail;" + "SELECT ID, DeviceAcronym, Label, Type, Phase, DestinationPhasorID, SourceIndex, BaseKV, UpdatedOn FROM PhasorDetail;" + "SELECT VersionNumber FROM SchemaVersion"; + #endif /// /// Default value for . @@ -801,17 +794,21 @@ private void UpdateInputMeasurementKeys() internal const int CipherSaltLength = 8; // Fields - private IServer m_serverCommandChannel; - private IClient m_clientCommandChannel; - private CertificatePolicyChecker m_certificateChecker; - private Dictionary m_subscriberIdentities; + private IServer? m_serverCommandChannel; + private IClient? m_clientCommandChannel; + private CertificatePolicyChecker? m_certificateChecker; + private Dictionary? m_subscriberIdentities; private readonly ConcurrentDictionary m_clientPublicationChannels; private readonly Dictionary> m_clientNotifications; +#if NET + private readonly Lock m_clientNotificationsLock; +#else private readonly object m_clientNotificationsLock; - private SharedTimer m_cipherKeyRotationTimer; +#endif + private readonly SharedTimer m_cipherKeyRotationTimer; + private readonly RoutingTables m_routingTables; private long m_commandChannelConnectionAttempts; private Guid? m_proxyClientID; - private RoutingTables m_routingTables; private bool m_encryptPayload; private long m_totalMeasurementsPerSecond; @@ -839,7 +836,7 @@ public DataPublisher() ClientConnections = new ConcurrentDictionary(); m_clientPublicationChannels = new ConcurrentDictionary(); m_clientNotifications = new Dictionary>(); - m_clientNotificationsLock = new object(); + m_clientNotificationsLock = new(); SecurityMode = DefaultSecurityMode; m_encryptPayload = DefaultEncryptPayload; SharedDatabase = DefaultSharedDatabase; @@ -855,7 +852,7 @@ public DataPublisher() m_routingTables = OptimizationOptions.DefaultRoutingMethod switch { OptimizationOptions.RoutingMethod.HighLatencyLowCpu => new RoutingTables(new RouteMappingHighLatencyLowCpu()), - _ => new RoutingTables() + _ => new RoutingTables() }; } @@ -880,6 +877,7 @@ public DataPublisher() [ConnectionStringParameter] [Description("Define the security mode used for communications over the command channel.")] [DefaultValue(DefaultSecurityMode)] + [Label("Security Mode")] public SecurityMode SecurityMode { get; set; } /// @@ -900,6 +898,7 @@ public DataPublisher() [ConnectionStringParameter] [Description("Define the flag that determines whether data sent over the data channel should be encrypted.")] [DefaultValue(DefaultEncryptPayload)] + [Label("Encrypt Payload")] public bool EncryptPayload { get => m_encryptPayload; @@ -908,7 +907,7 @@ public bool EncryptPayload m_encryptPayload = value; // Start cipher key rotation timer when encrypting payload - if (m_cipherKeyRotationTimer is not null) + if (!m_disposed) m_cipherKeyRotationTimer.Enabled = value; } } @@ -920,6 +919,7 @@ public bool EncryptPayload [ConnectionStringParameter] [Description("Define the flag that indicates whether this publisher is publishing data that this node subscribed to from another node in a shared database.")] [DefaultValue(DefaultSharedDatabase)] + [Label("Shared Database")] public bool SharedDatabase { get; set; } /// @@ -928,6 +928,7 @@ public bool EncryptPayload [ConnectionStringParameter] [Description("Define the flag that indicates if this publisher will allow payload compression when requested by subscribers.")] [DefaultValue(DefaultAllowPayloadCompression)] + [Label("Allow Payload Compression")] public bool AllowPayloadCompression { get; set; } /// @@ -936,6 +937,7 @@ public bool EncryptPayload [ConnectionStringParameter] [Description("Define the flag that indicates if this publisher will allow metadata refresh commands when requested by subscribers.")] [DefaultValue(DefaultAllowMetadataRefresh)] + [Label("Allow Metadata Refresh")] public bool AllowMetadataRefresh { get; set; } /// @@ -944,6 +946,7 @@ public bool EncryptPayload [ConnectionStringParameter] [Description("Define the flag that indicates if this publisher will allow filtering of data which is not a number.")] [DefaultValue(DefaultAllowNaNValueFilter)] + [Label("Allow NaN Value Filter")] public bool AllowNaNValueFilter { get; set; } /// @@ -952,6 +955,7 @@ public bool EncryptPayload [ConnectionStringParameter] [Description("Define the flag that indicates if this publisher will force filtering of data which is not a number.")] [DefaultValue(DefaultForceNaNValueFilter)] + [Label("Force NaN Value Filter")] public bool ForceNaNValueFilter { get; set; } /// @@ -960,6 +964,7 @@ public bool EncryptPayload [ConnectionStringParameter] [Description("Define the flag that determines whether to use base time offsets to decrease the size of compact measurements.")] [DefaultValue(DefaultUseBaseTimeOffsets)] + [Label("Force NaN Value Filter")] public bool UseBaseTimeOffsets { get; set; } /// @@ -968,11 +973,16 @@ public bool EncryptPayload [ConnectionStringParameter] [Description("Defines the maximum packet size to use for data publications. This number should be set as small as possible to reduce fragmentation, but large enough to keep large data flows from falling behind.")] [DefaultValue(DefaultMaxPacketSize)] + [Label("Max Packet Size")] public int MaxPacketSize { get; set; } = DefaultMaxPacketSize; + /// + /// Get or sets the size of the buffer used for sending and receiving data from clients. + /// [ConnectionStringParameter] - [Description("The size of the buffer used for sending and receiving data from clients.")] + [Description("Define the size of the buffer used for sending and receiving data from clients.")] [DefaultValue(DefaultBufferSize)] + [Label("Buffer Size")] public int BufferSize { get; set; } = DefaultBufferSize; /// @@ -981,24 +991,16 @@ public bool EncryptPayload [ConnectionStringParameter] [Description("Define the period, in milliseconds, over which new cipher keys will be provided to subscribers when EncryptPayload is true.")] [DefaultValue(DefaultCipherKeyRotationPeriod)] + [Label("Cipher Key Rotation Period")] public double CipherKeyRotationPeriod { - get - { - if (m_cipherKeyRotationTimer is not null) - return m_cipherKeyRotationTimer.Interval; - - return double.NaN; - } + get => m_cipherKeyRotationTimer.Interval; set { if (value < 1000.0D) throw new ArgumentOutOfRangeException(nameof(value), "Cipher key rotation period should not be set to less than 1000 milliseconds."); - if (m_cipherKeyRotationTimer is not null) - m_cipherKeyRotationTimer.Interval = (int)value; - else - throw new ArgumentException("Cannot assign new cipher rotation period, timer is not defined."); + m_cipherKeyRotationTimer.Interval = (int)value; } } @@ -1008,9 +1010,12 @@ public double CipherKeyRotationPeriod /// [ConnectionStringParameter] [Description("Defines the set of measurements to be cached and sent to subscribers immediately upon subscription.")] +#if !NET [CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")] +#endif [DefaultValue("")] - public string CachedMeasurementExpression { get; set; } + [Label("Cached Measurement Expression")] + public string CachedMeasurementExpression { get; set; } = ""; /// /// Gets or sets the measurement reporting interval. @@ -1021,6 +1026,7 @@ public double CipherKeyRotationPeriod [ConnectionStringParameter] [Description("Defines the measurement reporting interval used to determined how many measurements should be processed, per subscriber, before reporting status.")] [DefaultValue(AdapterBase.DefaultMeasurementReportingInterval)] + [Label("Measurement Reporting Interval")] public int MeasurementReportingInterval { get; set; } /// @@ -1029,6 +1035,7 @@ public double CipherKeyRotationPeriod [ConnectionStringParameter] [Description("Defines the maximum publication interval in milliseconds for data publications. Set to zero for no defined maximum.")] [DefaultValue(DefaultMaxPublishInterval)] + [Label("Max Publish Interval")] public long MaxPublishInterval { get; set; } = DefaultMaxPublishInterval; /// @@ -1037,6 +1044,7 @@ public double CipherKeyRotationPeriod [ConnectionStringParameter] [Description("Define the flag that determines whether measurement rights validation is enforced. Defaults to true for TLS connections.")] [DefaultValue(DefaultValidateMeasurementRights)] + [Label("Validate Measurement Rights")] public bool ValidateMeasurementRights { get; set; } /// @@ -1045,8 +1053,10 @@ public double CipherKeyRotationPeriod [ConnectionStringParameter] [Description("Define the flag that determines whether client subscriber IP address is validated. Defaults to true when measurement rights are validated.")] [DefaultValue(DefaultValidateClientIPAddress)] + [Label("Validate Client IP Address")] public bool ValidateClientIPAddress { get; set; } +#if !NET /// /// Gets or sets flag that determines if a should be used for reverse connections. /// @@ -1054,12 +1064,13 @@ public double CipherKeyRotationPeriod [DefaultValue(DefaultUseSimpleTcpClient)] [Description("Define the flag that determines whether a simple TCP client should be used for reverse connections.")] public bool UseSimpleTcpClient { get; set; } = DefaultUseSimpleTcpClient; +#endif /// /// Gets or sets based data source used to load each . /// Updates to this property will cascade to all items in this . /// - public override DataSet DataSource + public override DataSet? DataSource { get => base.DataSource; set @@ -1092,10 +1103,13 @@ public override string Status status.Append(m_serverCommandChannel?.Status); status.Append(m_clientCommandChannel?.Status); + #if !NET if (m_clientCommandChannel is not null) status.AppendLine($" Using simple TCP client: {UseSimpleTcpClient}"); + #endif status.Append(base.Status); + status.AppendLine($" Mutual Subscription: {MutualSubscription}"); status.AppendLine($" Reporting interval: {MeasurementReportingInterval:N0} per subscriber"); status.AppendLine($" Buffer block retransmits: {BufferBlockRetransmissions:N0}"); @@ -1119,8 +1133,10 @@ public override string Name // Only server command channel settings are persisted to config file base.Name = value.ToUpper(); + #if !NET if (m_serverCommandChannel is IPersistSettings commandChannel) commandChannel.SettingsCategory = value.Replace("!", "").ToLower(); + #endif } } @@ -1130,6 +1146,7 @@ public override string Name [ConnectionStringParameter] [Description("Semi-colon separated list of SQL select statements used to create data for meta-data exchange.")] [DefaultValue(DefaultMetadataTables)] + [Label("Metadata Tables")] public string MetadataTables { get; set; } /// @@ -1153,6 +1170,7 @@ public override string Name [ConnectionStringParameter] [Description("Gets or sets flag that determines if a subscription is mutual, i.e., bi-directional pub/sub.")] [DefaultValue(DefaultMutualSubscription)] + [Label("Mutual Subscription")] public bool MutualSubscription { get; set; } /// @@ -1164,12 +1182,12 @@ public override string Name /// /// Gets dictionary of connected clients. /// - protected internal ConcurrentDictionary ClientConnections { get; private set; } + protected internal ConcurrentDictionary ClientConnections { get; } /// /// Gets or sets reference to command channel, attaching and/or detaching to events as needed. /// - protected IServer ServerCommandChannel + protected IServer? ServerCommandChannel { get => m_serverCommandChannel; set @@ -1214,7 +1232,7 @@ protected IServer ServerCommandChannel /// /// This handles reverse connectivity operations. /// - protected IClient ClientCommandChannel + protected IClient? ClientCommandChannel { get => m_clientCommandChannel; set @@ -1321,30 +1339,21 @@ protected override void Dispose(bool disposing) ServerCommandChannel = null; - ClientConnections?.Values.AsParallel().ForAll(cc => cc.Dispose()); + ClientConnections.Values.AsParallel().ForAll(cc => cc.Dispose()); + ClientConnections.Clear(); - ClientConnections = null; - - if (m_routingTables is not null) - { - m_routingTables.StatusMessage -= RoutingTables_StatusMessage; - m_routingTables.ProcessException -= RoutingTables_ProcessException; - m_routingTables.Dispose(); - } - m_routingTables = null; + m_routingTables.StatusMessage -= RoutingTables_StatusMessage; + m_routingTables.ProcessException -= RoutingTables_ProcessException; + m_routingTables.Dispose(); // Dispose the cipher key rotation timer - if (m_cipherKeyRotationTimer is not null) - { - m_cipherKeyRotationTimer.Elapsed -= CipherKeyRotationTimer_Elapsed; - m_cipherKeyRotationTimer.Dispose(); - } - m_cipherKeyRotationTimer = null; + m_cipherKeyRotationTimer.Elapsed -= CipherKeyRotationTimer_Elapsed; + m_cipherKeyRotationTimer.Dispose(); } finally { - m_disposed = true; // Prevent duplicate dispose. - base.Dispose(disposing); // Call base class Dispose(). + m_disposed = true; // Prevent duplicate dispose. + base.Dispose(disposing); // Call base class Dispose(). } } @@ -1362,7 +1371,7 @@ public override void Initialize() Dictionary settings = Settings; // Check flag that will determine if subscriber payloads should be encrypted by default - if (settings.TryGetValue(nameof(EncryptPayload), out string setting)) + if (settings.TryGetValue(nameof(EncryptPayload), out string? setting)) m_encryptPayload = setting.ParseBoolean(); // Check flag that indicates whether publisher is publishing data @@ -1463,11 +1472,9 @@ public override void Initialize() bool clientBasedConnection = false; // Attempt to retrieve any defined command channel settings - Dictionary commandChannelSettings = settings.TryGetValue("commandChannel", out string commandChannelConnectionString) ? - commandChannelConnectionString.ParseKeyValuePairs() : - settings; + Dictionary commandChannelSettings = settings.TryGetValue("commandChannel", out string? commandChannelConnectionString) ? commandChannelConnectionString.ParseKeyValuePairs() : settings; - if (commandChannelSettings.TryGetValue("server", out string server)) + if (commandChannelSettings.TryGetValue("server", out string? server)) clientBasedConnection = !string.IsNullOrWhiteSpace(server); if (!commandChannelSettings.TryGetValue("bufferSize", out setting) || !int.TryParse(setting, out bufferSize)) @@ -1476,8 +1483,10 @@ public override void Initialize() if (bufferSize == 0) bufferSize = BufferSize; + #if !NET if (settings.TryGetValue(nameof(UseSimpleTcpClient), out setting)) UseSimpleTcpClient = setting.ParseBoolean(); + #endif if (SecurityMode == SecurityMode.TLS) { @@ -1490,7 +1499,7 @@ public override void Initialize() { bool checkCertificateRevocation; - if (!commandChannelSettings.TryGetValue("localCertificate", out string localCertificate) || !File.Exists(localCertificate)) + if (!commandChannelSettings.TryGetValue("localCertificate", out string? localCertificate) || !File.Exists(localCertificate)) localCertificate = DataSubscriber.GetLocalCertificate(); if (commandChannelSettings.TryGetValue("remoteCertificate", out setting)) @@ -1513,14 +1522,16 @@ public override void Initialize() PayloadAware = true, PayloadMarker = null, PayloadEndianOrder = EndianOrder.BigEndian, - PersistSettings = false, MaxConnectionAttempts = 1, - CertificateFile = FilePath.GetAbsolutePath(localCertificate), + CertificateFile = FilePath.GetAbsolutePath(localCertificate!), CheckCertificateRevocation = checkCertificateRevocation, CertificateChecker = m_certificateChecker, SendBufferSize = BufferSize, ReceiveBufferSize = bufferSize, - NoDelay = true + NoDelay = true, + #if !NET + PersistSettings = false, + #endif }; // Assign command channel client reference and attach to needed events @@ -1531,17 +1542,19 @@ public override void Initialize() // Create a new TLS server TlsServer commandChannel = new() { - SettingsCategory = Name.Replace("!", "").ToLower(), ConfigurationString = "port=7167", PayloadAware = true, PayloadMarker = null, PayloadEndianOrder = EndianOrder.BigEndian, RequireClientCertificate = true, CertificateChecker = m_certificateChecker, - PersistSettings = true, SendBufferSize = BufferSize, ReceiveBufferSize = BufferSize, - NoDelay = true + NoDelay = true, + #if !NET + PersistSettings = true, + SettingsCategory = Name.Replace("!", "").ToLower(), + #endif }; // Assign command channel server reference and attach to needed events @@ -1552,6 +1565,7 @@ public override void Initialize() { if (clientBasedConnection) { + #if !NET if (UseSimpleTcpClient) { // Create a new simple TCP client @@ -1572,37 +1586,44 @@ public override void Initialize() } else { - // Create a new TCP client - TcpClient commandChannel = new() - { - PayloadAware = true, - PayloadMarker = null, - PayloadEndianOrder = EndianOrder.BigEndian, - PersistSettings = false, - MaxConnectionAttempts = -1, - SendBufferSize = BufferSize, - ReceiveBufferSize = bufferSize, - NoDelay = true - }; + #endif + // Create a new TCP client + TcpClient commandChannel = new() + { + PayloadAware = true, + PayloadMarker = null, + PayloadEndianOrder = EndianOrder.BigEndian, + MaxConnectionAttempts = -1, + SendBufferSize = BufferSize, + ReceiveBufferSize = bufferSize, + NoDelay = true, + #if !NET + PersistSettings = false, + #endif + }; - // Assign command channel client reference and attach to needed events - ClientCommandChannel = commandChannel; + // Assign command channel client reference and attach to needed events + ClientCommandChannel = commandChannel; + #if !NET } + #endif } else { // Create a new TCP server TcpServer commandChannel = new() { - SettingsCategory = Name.Replace("!", "").ToLower(), ConfigurationString = "port=7165", PayloadAware = true, PayloadMarker = null, PayloadEndianOrder = EndianOrder.BigEndian, - PersistSettings = true, SendBufferSize = BufferSize, ReceiveBufferSize = BufferSize, - NoDelay = true + NoDelay = true, + #if !NET + PersistSettings = true, + SettingsCategory = Name.Replace("!", "").ToLower(), + #endif }; // Assign command channel server reference and attach to needed events @@ -1613,12 +1634,12 @@ public override void Initialize() if (clientBasedConnection) { // Set client-based connection string - m_clientCommandChannel.ConnectionString = commandChannelConnectionString ?? ConnectionString; + m_clientCommandChannel!.ConnectionString = commandChannelConnectionString ?? ConnectionString; } else { // Initialize TCP server - this will load persisted settings - m_serverCommandChannel.Initialize(); + m_serverCommandChannel!.Initialize(); // Allow user to override persisted server settings by specifying a command channel setting if (!string.IsNullOrWhiteSpace(commandChannelConnectionString)) @@ -1627,7 +1648,7 @@ public override void Initialize() // Start cipher key rotation timer when encrypting payload if (m_encryptPayload) - m_cipherKeyRotationTimer?.Start(); + m_cipherKeyRotationTimer.Start(); // Register publisher with the statistics engine StatisticsEngine.Register(this, "Publisher", "PUB"); @@ -1645,7 +1666,7 @@ RoutingPassthroughMethod IOptimizedRoutingConsumer.GetRoutingPassthroughMethods( /// Queues a collection of measurements for processing to each connected to this . /// /// Measurements to queue for processing. - public override void QueueMeasurementsForProcessing(IEnumerable measurements) + public override void QueueMeasurementsForProcessing(IEnumerable? measurements) { if (measurements is null) return; @@ -1666,7 +1687,7 @@ public override void QueueMeasurementsForProcessing(IEnumerable me /// Queues a collection of measurements for processing to each connected to this . /// /// Measurements to queue for processing. - private void QueueMeasurementsForProcessing(List measurements) + private void QueueMeasurementsForProcessing(List? measurements) { if (measurements is null || measurements.Count == 0) return; @@ -1725,33 +1746,37 @@ public override string GetShortStatus(int maxLength) /// /// Enumerates connected clients. /// - [AdapterCommand("Enumerates connected clients.", "Administrator", "Editor", "Viewer")] - public virtual void EnumerateClients() + [AdapterCommand("Enumerates connected clients.")] + [Label("Enumerate Clients")] + public virtual string EnumerateClients() { - OnStatusMessage(MessageLevel.Info, EnumerateClients(false)); + string result = EnumerateClients(false); + OnStatusMessage(MessageLevel.Info, result); + return result; } /// /// Enumerates connected clients with active temporal sessions. /// - [AdapterCommand("Enumerates connected clients with active temporal sessions.", "Administrator", "Editor", "Viewer")] - public virtual void EnumerateTemporalClients() + [AdapterCommand("Enumerates connected clients with active temporal sessions.")] + [Label("Enumerate Temporal Clients")] + public virtual string EnumerateTemporalClients() { - OnStatusMessage(MessageLevel.Info, EnumerateClients(true)); + string result = EnumerateClients(true); + OnStatusMessage(MessageLevel.Info, result); + return result; } private string EnumerateClients(bool filterToTemporalSessions) { StringBuilder clientEnumeration = new(); - Guid[] clientIDs = (Guid[])m_serverCommandChannel?.ClientIDs.Clone() ?? [m_proxyClientID.GetValueOrDefault()]; + Guid[] clientIDs = (Guid[]?)m_serverCommandChannel?.ClientIDs.Clone() ?? [m_proxyClientID.GetValueOrDefault()]; - clientEnumeration.AppendLine(filterToTemporalSessions ? - $"{Environment.NewLine}Indices for connected clients with active temporal sessions:{Environment.NewLine}" : - $"{Environment.NewLine}Indices for {clientIDs.Length:N0} connected clients:{Environment.NewLine}"); + clientEnumeration.AppendLine(filterToTemporalSessions ? $"{Environment.NewLine}Indices for connected clients with active temporal sessions:{Environment.NewLine}" : $"{Environment.NewLine}Indices for {clientIDs.Length:N0} connected clients:{Environment.NewLine}"); for (int i = 0; i < clientIDs.Length; i++) { - if (!ClientConnections.TryGetValue(clientIDs[i], out SubscriberConnection connection) || connection?.Subscription is null) + if (!ClientConnections.TryGetValue(clientIDs[i], out SubscriberConnection? connection) || connection.Subscription is null) continue; bool hasActiveTemporalSession = connection.Subscription.TemporalConstraintIsDefined(); @@ -1773,7 +1798,9 @@ private string EnumerateClients(bool filterToTemporalSessions) /// Rotates cipher keys for specified client connection. /// /// Enumerated index for client connection. - [AdapterCommand("Rotates cipher keys for client connection using its enumerated index.", "Administrator")] + [AdapterCommand("Rotates cipher keys for client connection using its enumerated index.")] + [Label("Rotate Cipher Keys")] + [Parameter(nameof(clientIndex), "Client Index", "Enumerated index for client connection.")] public virtual void RotateCipherKeys(int clientIndex) { Guid clientID = Guid.Empty; @@ -1791,8 +1818,8 @@ public virtual void RotateCipherKeys(int clientIndex) if (!success) return; - - if (ClientConnections.TryGetValue(clientID, out SubscriberConnection connection)) + + if (ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection)) connection.RotateCipherKeys(); else OnStatusMessage(MessageLevel.Error, $"Failed to find connected client {clientID}"); @@ -1802,7 +1829,8 @@ public virtual void RotateCipherKeys(int clientIndex) /// Gets subscriber information for specified client connection. /// /// Enumerated index for client connection. - [AdapterCommand("Gets subscriber information for client connection using its enumerated index.", "Administrator", "Editor", "Viewer")] + [AdapterCommand("Gets subscriber information for client connection using its enumerated index.")] + [Label("Get Subscriber Info")] public virtual string GetSubscriberInfo(int clientIndex) { Guid clientID = Guid.Empty; @@ -1820,7 +1848,7 @@ public virtual string GetSubscriberInfo(int clientIndex) if (success) { - if (ClientConnections.TryGetValue(clientID, out SubscriberConnection connection)) + if (ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection)) return connection.SubscriberInfo; OnStatusMessage(MessageLevel.Error, $"Failed to find connected client {clientID}"); @@ -1833,7 +1861,9 @@ public virtual string GetSubscriberInfo(int clientIndex) /// Gets temporal status for a specified client connection. /// /// Enumerated index for client connection. - [AdapterCommand("Gets temporal status for a subscriber, if any, using its enumerated index.", "Administrator", "Editor", "Viewer")] + [AdapterCommand("Gets temporal status for a subscriber, if any, using its enumerated index.")] + [Label("Get Temporal Status")] + [Parameter(nameof(clientIndex), "Client Index", "Enumerated index for client connection.")] public virtual string GetTemporalStatus(int clientIndex) { Guid clientID = Guid.Empty; @@ -1851,15 +1881,13 @@ public virtual string GetTemporalStatus(int clientIndex) if (success) { - if (ClientConnections.TryGetValue(clientID, out SubscriberConnection connection)) + if (ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection)) { - string temporalStatus = null; + string? temporalStatus = null; if (connection.Subscription is not null) { - temporalStatus = connection.Subscription.TemporalConstraintIsDefined() ? - connection.Subscription.TemporalSessionStatus : - "Subscription does not have an active temporal session."; + temporalStatus = connection.Subscription.TemporalConstraintIsDefined() ? connection.Subscription.TemporalSessionStatus : "Subscription does not have an active temporal session."; } if (string.IsNullOrWhiteSpace(temporalStatus)) @@ -1878,10 +1906,11 @@ public virtual string GetTemporalStatus(int clientIndex) /// Gets the local certificate currently in use by the data publisher. /// /// The local certificate file read directly from the certificate file as an array of bytes. - [AdapterCommand("Gets the local certificate currently in use by the data publisher.", "Administrator", "Editor")] + [AdapterCommand("Gets the local certificate currently in use by the data publisher.")] + [Label("Get Local Certificate")] public virtual byte[] GetLocalCertificate() { - if (m_serverCommandChannel is not TlsServer commandChannel) + if (m_serverCommandChannel is not TlsServer commandChannel || string.IsNullOrWhiteSpace(commandChannel.CertificateFile)) throw new InvalidOperationException("Certificates can only be exported in TLS security mode with a server-based command channel."); return File.ReadAllBytes(FilePath.GetAbsolutePath(commandChannel.CertificateFile)); @@ -1893,7 +1922,10 @@ public virtual byte[] GetLocalCertificate() /// The file name to give to the certificate when imported. /// The data to be written to the certificate file. /// The local path on the server where the file was written. - [AdapterCommand("Imports a certificate to the trusted certificates path.", "Administrator", "Editor")] + [AdapterCommand("Imports a certificate to the trusted certificates path.")] + [Label("Import Certificate")] + [Parameter(nameof(fileName), "File Name", "The file name to give to the certificate when imported.")] + [Parameter(nameof(certificateData), "Certificate Data", "The data to be written to the certificate file.")] public virtual string ImportCertificate(string fileName, byte[] certificateData) { if (m_serverCommandChannel is not TlsServer commandChannel) @@ -1915,18 +1947,21 @@ public virtual string ImportCertificate(string fileName, byte[] certificateData) /// Gets subscriber status for specified subscriber ID. /// /// Guid based subscriber ID for client connection. - [AdapterCommand("Gets subscriber status for client connection using its subscriber ID.", "Administrator", "Editor", "Viewer")] + [AdapterCommand("Gets subscriber status for client connection using its subscriber ID.")] + [Label("Get Subscriber Status")] + [Parameter(nameof(subscriberID), "Subscriber ID", "Guid based subscriber ID for client connection.")] public virtual Tuple GetSubscriberStatus(Guid subscriberID) { - return new Tuple(subscriberID, - GetConnectionProperty(subscriberID, sc => sc.IsConnected), - GetConnectionProperty(subscriberID, sc => sc.SubscriberInfo)); + return new Tuple(subscriberID, + GetConnectionProperty(subscriberID, sc => sc.IsConnected), + GetConnectionProperty(subscriberID, sc => sc.SubscriberInfo) ?? "undefined"); } /// /// Resets the counters for the lifetime statistics without interrupting the adapter's operations. /// - [AdapterCommand("Resets the counters for the lifetime statistics without interrupting the adapter's operations.", "Administrator", "Editor")] + [AdapterCommand("Resets the counters for the lifetime statistics without interrupting the adapter's operations.")] + [Label("Reset Lifetime Counters")] public virtual void ResetLifetimeCounters() { LifetimeMeasurements = 0L; @@ -1941,7 +1976,9 @@ public virtual void ResetLifetimeCounters() /// Sends a notification to all subscribers. /// /// The message to be sent. - [AdapterCommand("Sends a notification to all subscribers.", "Administrator", "Editor")] + [AdapterCommand("Sends a notification to all subscribers.")] + [Label("Send Notification")] + [Parameter(nameof(message), "Message", "The message to be sent.")] public virtual void SendNotification(string message) { string notification = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.fff}] {message}"; @@ -1964,7 +2001,7 @@ public virtual void SendNotification(string message) /// Client ID of connection over which to update signal index cache. /// New signal index cache. /// Subscribed measurement keys. - public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndexCache, MeasurementKey[] inputMeasurementKeys) + public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache? signalIndexCache, MeasurementKey[]? inputMeasurementKeys) { ConcurrentDictionary reference = new(); List unauthorizedKeys = []; @@ -1972,9 +2009,7 @@ public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndex if (inputMeasurementKeys is not null) { - Func hasRightsFunc = ValidateMeasurementRights ? - new SubscriberRightsLookup(DataSource, signalIndexCache.SubscriberID).HasRightsFunc : - _ => true; + Func hasRightsFunc = ValidateMeasurementRights ? new SubscriberRightsLookup(DataSource, signalIndexCache?.SubscriberID ?? Guid.Empty).HasRightsFunc : _ => true; // We will now go through the client's requested keys and see which ones are authorized for subscription, // this information will be available through the returned signal index cache which will also define @@ -1992,7 +2027,7 @@ public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndex } // Send client updated signal index cache - if (ClientConnections.TryGetValue(clientID, out SubscriberConnection connection)) + if (ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection)) { if (connection.Version > 1) { @@ -2002,7 +2037,7 @@ public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndex UnauthorizedSignalIDs = unauthorizedKeys.ToArray() }; - byte[] serializedSignalIndexCache = SerializeSignalIndexCache(clientID, nextSignalIndexCache); + byte[]? serializedSignalIndexCache = SerializeSignalIndexCache(clientID, nextSignalIndexCache); if (serializedSignalIndexCache is null || serializedSignalIndexCache.Length == 0) { @@ -2013,7 +2048,7 @@ public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndex lock (connection.CacheUpdateLock) { // Update primary signal cache index at startup - if (connection.SignalIndexCache.RefreshCount == 0) + if (connection.SignalIndexCache is not null && connection.SignalIndexCache.RefreshCount == 0) { connection.SignalIndexCache.Reference = reference; connection.SignalIndexCache.UnauthorizedSignalIDs = unauthorizedKeys.ToArray(); @@ -2041,15 +2076,17 @@ public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndex } } } - else + else if (signalIndexCache is not null) { signalIndexCache.Reference = reference; signalIndexCache.UnauthorizedSignalIDs = unauthorizedKeys.ToArray(); - + if (connection.IsSubscribed) { - byte[] serializedSignalIndexCache = SerializeSignalIndexCache(clientID, signalIndexCache); - SendClientResponse(clientID, ServerResponse.UpdateSignalIndexCache, ServerCommand.Subscribe, serializedSignalIndexCache); + byte[]? serializedSignalIndexCache = SerializeSignalIndexCache(clientID, signalIndexCache); + + if (serializedSignalIndexCache is not null) + SendClientResponse(clientID, ServerResponse.UpdateSignalIndexCache, ServerCommand.Subscribe, serializedSignalIndexCache); } } } @@ -2065,13 +2102,13 @@ protected void UpdateLatestMeasurementCache() { try { - if (!Settings.TryGetValue(nameof(CachedMeasurementExpression), out string cachedMeasurementExpression)) + if (!Settings.TryGetValue(nameof(CachedMeasurementExpression), out string? cachedMeasurementExpression)) return; - if (!TryGetAdapterByName(nameof(LatestMeasurementCache), out IActionAdapter cache)) + if (!TryGetAdapterByName(nameof(LatestMeasurementCache), out IActionAdapter? cache)) return; - cache.InputMeasurementKeys = AdapterBase.ParseInputMeasurementKeys(DataSource, true, cachedMeasurementExpression); + cache!.InputMeasurementKeys = AdapterBase.ParseInputMeasurementKeys(DataSource, true, cachedMeasurementExpression); m_routingTables.CalculateRoutingTables(null); } catch (Exception ex) @@ -2100,9 +2137,9 @@ private void UpdateClientNotifications() { m_clientNotifications.Clear(); - if (DataSource.Tables.Contains("Subscribers")) + if (DataSource?.Tables.Contains("Subscribers") ?? false) { - foreach (DataRow row in DataSource.Tables["Subscribers"].Rows) + foreach (DataRow row in DataSource.Tables["Subscribers"]!.Rows) { if (Guid.TryParse(row["ID"].ToNonNullString(), out Guid subscriberID)) m_clientNotifications.Add(subscriberID, new Dictionary()); @@ -2160,17 +2197,17 @@ private void DeserializeClientNotifications() using FileStream fileStream = File.OpenRead(notificationsFileName); using TextReader reader = new StreamReader(fileStream); - string line = reader.ReadLine(); + string? line = reader.ReadLine(); while (line is not null) { int separatorIndex = line.IndexOf(','); - if (Guid.TryParse(line.Substring(0, separatorIndex), out Guid subscriberID)) + if (Guid.TryParse(line[..separatorIndex], out Guid subscriberID)) { - if (m_clientNotifications.TryGetValue(subscriberID, out Dictionary notifications)) + if (m_clientNotifications.TryGetValue(subscriberID, out Dictionary? notifications) /* && notifications is not null */) { - string notification = line.Substring(separatorIndex + 1); + string notification = line[(separatorIndex + 1)..]; notifications.Add(notification.GetHashCode(), notification); } } @@ -2189,7 +2226,7 @@ private void SendNotifications(SubscriberConnection connection) { using BlockAllocatedMemoryStream buffer = new(); - if (!m_clientNotifications.TryGetValue(connection.SubscriberID, out Dictionary notifications)) + if (!m_clientNotifications.TryGetValue(connection.SubscriberID, out Dictionary? notifications) /* || notifications is null */) return; foreach (KeyValuePair pair in notifications) @@ -2211,12 +2248,7 @@ private void SendNotifications(SubscriberConnection connection) /// Text encoding associated with a particular client. protected internal Encoding GetClientEncoding(Guid clientID) { - if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection connection)) - return Encoding.UTF8; - - Encoding clientEncoding = connection.Encoding; - - return clientEncoding ?? Encoding.UTF8; + return !ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection) ? Encoding.UTF8 : connection.Encoding; } /// @@ -2228,7 +2260,7 @@ protected internal virtual bool SendDataStartTime(Guid clientID, Ticks startTime { bool result = SendClientResponse(clientID, ServerResponse.DataStartTime, ServerCommand.Subscribe, BigEndian.GetBytes((long)startTime)); - if (ClientConnections.TryGetValue(clientID, out SubscriberConnection connection)) + if (ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection) /* && connection is not null */) OnStatusMessage(MessageLevel.Info, $"Start time sent to {connection.ConnectionID}."); return result; @@ -2245,7 +2277,7 @@ protected internal virtual bool SendDataStartTime(Guid clientID, Ticks startTime /// true if send was successful; otherwise false. protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command) { - return SendClientResponse(clientID, response, command, (byte[])null); + return SendClientResponse(clientID, response, command, (byte[]?)null); } /// @@ -2256,11 +2288,9 @@ protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse /// In response to command. /// Status message to return. /// true if send was successful; otherwise false. - protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command, string status) + protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command, string? status) { - return status is null ? - SendClientResponse(clientID, response, command) : - SendClientResponse(clientID, response, command, GetClientEncoding(clientID).GetBytes(status)); + return status is null ? SendClientResponse(clientID, response, command) : SendClientResponse(clientID, response, command, GetClientEncoding(clientID).GetBytes(status)); } /// @@ -2274,9 +2304,7 @@ protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse /// true if send was successful; otherwise false. protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command, string formattedStatus, params object[] args) { - return string.IsNullOrWhiteSpace(formattedStatus) ? - SendClientResponse(clientID, response, command) : - SendClientResponse(clientID, response, command, GetClientEncoding(clientID).GetBytes(string.Format(formattedStatus, args))); + return string.IsNullOrWhiteSpace(formattedStatus) ? SendClientResponse(clientID, response, command) : SendClientResponse(clientID, response, command, GetClientEncoding(clientID).GetBytes(string.Format(formattedStatus, args))); } /// @@ -2287,7 +2315,7 @@ protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse /// In response to command. /// Data to return to client; null if none. /// true if send was successful; otherwise false. - protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command, byte[] data) + protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command, byte[]? data) { return SendClientResponse(clientID, (byte)response, (byte)command, data); } @@ -2318,7 +2346,7 @@ protected internal virtual void UpdateLatencyStatistics(IEnumerable latenc // Attempts to get the subscriber for the given client based on that client's X.509 certificate. private void TryFindClientDetails(SubscriberConnection connection) { - X509Certificate remoteCertificate; + X509Certificate? remoteCertificate; // If connection is not TLS, there is no X.509 certificate if (m_serverCommandChannel is not TlsServer serverCommandChannel) @@ -2327,22 +2355,22 @@ private void TryFindClientDetails(SubscriberConnection connection) return; // Get remote certificate and corresponding trusted certificate - remoteCertificate = clientCommandChannel.SslStream.RemoteCertificate; + remoteCertificate = clientCommandChannel.SslStream?.RemoteCertificate; } else { // If connection is not found, cannot get X.509 certificate - if (!serverCommandChannel.TryGetClient(connection.ClientID, out TransportProvider client)) + if (!serverCommandChannel.TryGetClient(connection.ClientID, out TransportProvider? client)) return; // Get remote certificate and corresponding trusted certificate - remoteCertificate = client.Provider.SslStream.RemoteCertificate; + remoteCertificate = client!.Provider?.SslStream?.RemoteCertificate; } if (remoteCertificate is null) return; - if (!m_subscriberIdentities.TryGetValue(m_certificateChecker.GetTrustedCertificate(remoteCertificate), out DataRow subscriber)) + if (m_certificateChecker is null || m_subscriberIdentities is null || !m_subscriberIdentities.TryGetValue(m_certificateChecker.GetTrustedCertificate(remoteCertificate)!, out DataRow? subscriber) /* || subscriber is null */) return; // Load client details from subscriber identity @@ -2357,13 +2385,13 @@ private void UpdateCertificateChecker() { try { - if (m_certificateChecker is null || m_subscriberIdentities is null || SecurityMode != SecurityMode.TLS) + if (m_certificateChecker is null || m_subscriberIdentities is null || SecurityMode != SecurityMode.TLS || DataSource is null || !DataSource.Tables.Contains("Subscribers")) return; m_certificateChecker.DistrustAll(); m_subscriberIdentities.Clear(); - foreach (DataRow subscriber in DataSource.Tables["Subscribers"].Select("Enabled <> 0")) + foreach (DataRow subscriber in DataSource.Tables["Subscribers"]!.Select("Enabled <> 0")) { try { @@ -2379,7 +2407,9 @@ private void UpdateCertificateChecker() if (!File.Exists(remoteCertificateFile)) continue; + #pragma warning disable SYSLIB0057 X509Certificate certificate = new X509Certificate2(remoteCertificateFile); + #pragma warning restore SYSLIB0057 m_certificateChecker.Trust(certificate, policy); m_subscriberIdentities.Add(certificate, subscriber); } @@ -2396,18 +2426,17 @@ private void UpdateCertificateChecker() } // Update rights for the given subscription. - private void UpdateRights(SubscriberConnection connection) + private void UpdateRights(SubscriberConnection? connection) { - if (connection is null) + if (connection is null || DataSource is null || !DataSource.Tables.Contains("Subscribers")) return; try { - SubscriberAdapter subscription = connection.Subscription; + SubscriberAdapter? subscription = connection.Subscription; // Determine if the connection has been disabled or removed - make sure to set authenticated to false if necessary - if (DataSource is not null && DataSource.Tables.Contains("Subscribers") && - !DataSource.Tables["Subscribers"].Select($"ID = '{connection.SubscriberID}' AND Enabled <> 0").Any()) + if (DataSource is not null && DataSource.Tables.Contains("Subscribers") && DataSource.Tables["Subscribers"]!.Select($"ID = '{connection.SubscriberID}' AND Enabled <> 0").Length == 0) connection.Authenticated = false; if (subscription is null) @@ -2415,12 +2444,10 @@ private void UpdateRights(SubscriberConnection connection) // It is important here that "SELECT" not be allowed in parsing the input measurement keys expression since this key comes // from the remote subscription - this will prevent possible SQL injection attacks. - MeasurementKey[] requestedInputs = AdapterBase.ParseInputMeasurementKeys(DataSource, false, subscription.RequestedInputFilter); + MeasurementKey[] requestedInputs = AdapterBase.ParseInputMeasurementKeys(DataSource, false, subscription.RequestedInputFilter ?? "FILTER ActiveMeasurements WHERE True"); HashSet authorizedSignals = []; - Func hasRightsFunc = ValidateMeasurementRights ? - new SubscriberRightsLookup(DataSource, subscription.SubscriberID).HasRightsFunc : - _ => true; + Func hasRightsFunc = ValidateMeasurementRights ? new SubscriberRightsLookup(DataSource, subscription.SubscriberID).HasRightsFunc : _ => true; foreach (MeasurementKey input in requestedInputs) { @@ -2428,7 +2455,7 @@ private void UpdateRights(SubscriberConnection connection) authorizedSignals.Add(input); } - if (authorizedSignals.SetEquals(subscription.InputMeasurementKeys)) + if (authorizedSignals.SetEquals(subscription.InputMeasurementKeys ?? [])) return; // Update the subscription associated with this connection based on newly acquired or revoked rights @@ -2446,13 +2473,13 @@ private void UpdateRights(SubscriberConnection connection) } // Send binary response packet to client - private bool SendClientResponse(Guid clientID, byte responseCode, byte commandCode, byte[] data) + private bool SendClientResponse(Guid clientID, byte responseCode, byte commandCode, byte[]? data) { // Attempt to lookup associated client connection - if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection connection) || connection is null || connection.ClientNotFoundExceptionOccurred) + if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection) /* || connection is null */ || connection.ClientNotFoundExceptionOccurred) return false; - BlockAllocatedMemoryStream workingBuffer = null; + BlockAllocatedMemoryStream? workingBuffer = null; bool success = false; try @@ -2529,12 +2556,10 @@ private bool SendClientResponse(Guid clientID, byte responseCode, byte commandCo if (m_clientCommandChannel is null) { // Data packets and buffer blocks can be published on a UDP data channel, so check for this... - IServer publishChannel = useDataChannel ? - m_clientPublicationChannels.GetOrAdd(clientID, _ => connection.ServerPublishChannel) : - m_serverCommandChannel; + IServer publishChannel = useDataChannel ? m_clientPublicationChannels.GetOrAdd(clientID, _ => connection.ServerPublishChannel!) : m_serverCommandChannel!; // Send response packet - if (publishChannel?.CurrentState == ServerState.Running) + if (publishChannel.CurrentState == ServerState.Running) { byte[] responseData = workingBuffer.ToArray(); @@ -2603,26 +2628,23 @@ private bool SendClientResponse(Guid clientID, byte responseCode, byte commandCo } // Socket exception handler - private bool HandleSocketException(Guid clientID, Exception ex) + private bool HandleSocketException(Guid clientID, Exception? ex) { - if (ex is SocketException socketException) + // WSAECONNABORTED and WSAECONNRESET are common errors after a client disconnect, + // if they happen for other reasons, make sure disconnect procedure is handled + if (ex is SocketException { ErrorCode: 10053 or 10054 }) { - // WSAECONNABORTED and WSAECONNRESET are common errors after a client disconnect, - // if they happen for other reasons, make sure disconnect procedure is handled - if (socketException.ErrorCode == 10053 || socketException.ErrorCode == 10054) + try { - try - { - ThreadPool.QueueUserWorkItem(DisconnectClient, clientID); - } - catch (Exception queueException) - { - // Process exception for logging - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Failed to queue client disconnect due to exception: {queueException.Message}", queueException)); - } - - return true; + ThreadPool.QueueUserWorkItem(DisconnectClient, clientID); + } + catch (Exception queueException) + { + // Process exception for logging + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Failed to queue client disconnect due to exception: {queueException.Message}", queueException)); } + + return true; } if (ex is not null) @@ -2632,8 +2654,11 @@ private bool HandleSocketException(Guid clientID, Exception ex) } // Disconnect client - this should be called from non-blocking thread (e.g., thread pool) - private void DisconnectClient(object state) + private void DisconnectClient(object? state) { + if (state is null) + return; + m_commandChannelConnectionAttempts = 0; try @@ -2642,13 +2667,11 @@ private void DisconnectClient(object state) RemoveClientSubscription(clientID); - if (ClientConnections.TryRemove(clientID, out SubscriberConnection connection)) + if (ClientConnections.TryRemove(clientID, out SubscriberConnection? connection)) { connection.Dispose(); - OnStatusMessage(MessageLevel.Info, clientID == m_proxyClientID ? - $"Data publisher client-based connection disconnected from subscriber via {m_clientCommandChannel.ServerUri}." : - "Client disconnected from command channel."); + OnStatusMessage(MessageLevel.Info, clientID == m_proxyClientID ? $"Data publisher client-based connection disconnected from subscriber via {m_clientCommandChannel?.ServerUri ?? "undefined server URI"}." : "Client disconnected from command channel."); } m_clientPublicationChannels.TryRemove(clientID, out _); @@ -2665,7 +2688,7 @@ private void RemoveClientSubscription(Guid clientID) { lock (this) { - if (!TryGetClientSubscription(clientID, out SubscriberAdapter clientSubscription)) + if (!TryGetClientSubscription(clientID, out SubscriberAdapter? clientSubscription) || clientSubscription is null) return; clientSubscription.Stop(); @@ -2685,18 +2708,18 @@ private void RemoveClientSubscription(Guid clientID) } // Handle notification on input measurement key change - private void NotifyHostOfSubscriptionRemoval(object state) + private void NotifyHostOfSubscriptionRemoval(object? _) { OnInputMeasurementKeysUpdated(); } // Attempt to find client subscription - private bool TryGetClientSubscription(Guid clientID, out SubscriberAdapter subscription) + private bool TryGetClientSubscription(Guid clientID, out SubscriberAdapter? subscription) { // Lookup adapter by its client ID - if (TryGetAdapter(clientID, GetClientSubscription, out IActionAdapter adapter)) + if (TryGetAdapter(clientID, GetClientSubscription, out IActionAdapter? adapter)) { - subscription = (SubscriberAdapter)adapter; + subscription = (SubscriberAdapter)adapter!; return true; } @@ -2710,12 +2733,12 @@ private bool GetClientSubscription(IActionAdapter item, Guid value) } // Gets specified property from client connection based on subscriber ID - private TResult GetConnectionProperty(Guid subscriberID, Func predicate) + private TResult? GetConnectionProperty(Guid subscriberID, Func predicate) { - TResult result = default; + TResult? result = default; // Lookup client connection by subscriber ID - SubscriberConnection connection = ClientConnections.Values.FirstOrDefault(cc => cc.SubscriberID == subscriberID); + SubscriberConnection? connection = ClientConnections.Values.FirstOrDefault(cc => cc.SubscriberID == subscriberID); // Extract desired property from client connection using given predicate function if (connection is not null) @@ -2759,43 +2782,66 @@ protected virtual void OnClientConnected(Guid subscriberID, string connectionID, } } - protected internal new void OnStatusMessage(MessageLevel level, string status, string eventName = null, MessageFlags flags = MessageFlags.None) + /// + /// Raises the event and sends this data to the . + /// + /// The to assign to this message + /// New status message. + /// A fixed string to classify this event; defaults to null. + /// to use, if any; defaults to . + /// + /// should be a constant string value associated with what type of message is being + /// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this + /// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred. + /// + // Re-exposed for internal use, see SubscriberConnection + protected internal new void OnStatusMessage(MessageLevel level, string status, string? eventName = null, MessageFlags flags = MessageFlags.None) { base.OnStatusMessage(level, status, eventName, flags); } - protected internal new void OnProcessException(MessageLevel level, Exception exception, string eventName = null, MessageFlags flags = MessageFlags.None) + /// + /// Raises the event. + /// + /// The to assign to this message + /// Processing . + /// A fixed string to classify this event; defaults to null. + /// to use, if any; defaults to . + /// + /// should be a constant string value associated with what type of message is being + /// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this + /// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred. + /// + // Re-exposed for internal use, see SubscriberConnection + protected internal new void OnProcessException(MessageLevel level, Exception exception, string? eventName = null, MessageFlags flags = MessageFlags.None) { base.OnProcessException(level, exception, eventName, flags); } // Make sure to expose any routing table messages - private void RoutingTables_StatusMessage(object sender, EventArgs e) + private void RoutingTables_StatusMessage(object? sender, EventArgs e) { OnStatusMessage(MessageLevel.Info, e.Argument); } // Make sure to expose any routing table exceptions - private void RoutingTables_ProcessException(object sender, EventArgs e) + private void RoutingTables_ProcessException(object? sender, EventArgs e) { OnProcessException(MessageLevel.Warning, e.Argument); } // Cipher key rotation timer handler - private void CipherKeyRotationTimer_Elapsed(object sender, EventArgs e) + private void CipherKeyRotationTimer_Elapsed(object? sender, EventArgs e) { - if (ClientConnections is null) - return; - foreach (SubscriberConnection connection in ClientConnections.Values) { - if (connection is not null && connection.Authenticated) + if (connection.Authenticated) connection.RotateCipherKeys(); } } // Determines whether the data in the data source has actually changed when receiving a new data source. - private bool DataSourceChanged(DataSet newDataSource) + private bool DataSourceChanged(DataSet? newDataSource) { try { @@ -2813,7 +2859,7 @@ private bool DataSourceChanged(DataSet newDataSource) private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buffer, int startIndex, int length) { Guid clientID = connection.ClientID; - SubscriberAdapter subscription; + SubscriberAdapter? subscription; string message; // Handle subscribe @@ -2856,7 +2902,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff // Update connection string settings for GSF adapter syntax: Dictionary settings = connectionString.ParseKeyValuePairs(); - if (settings.TryGetValue(nameof(SubscriptionInfo.Throttled), out string setting)) + if (settings.TryGetValue(nameof(SubscriptionInfo.Throttled), out string? setting)) settings[nameof(FacileActionAdapterBase.TrackLatestMeasurements)] = setting; if (settings.TryGetValue(nameof(SubscriptionInfo.FilterExpression), out setting)) @@ -2877,9 +2923,9 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff if (subscription.Settings.TryGetValue("dataChannel", out setting)) { - Socket clientSocket = connection.GetCommandChannelSocket(); + Socket? clientSocket = connection.GetCommandChannelSocket(); settings = setting.ParseKeyValuePairs(); - IPEndPoint localEndPoint = null; + IPEndPoint? localEndPoint = null; string networkInterface = "::0"; // Make sure return interface matches incoming client connection @@ -2892,7 +2938,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff // Remove dual-stack prefix if (networkInterface.StartsWith("::ffff:", true, CultureInfo.InvariantCulture)) - networkInterface = networkInterface.Substring(7); + networkInterface = networkInterface[7..]; } if (settings.TryGetValue("port", out setting) || settings.TryGetValue("localport", out setting)) @@ -2914,7 +2960,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff } // Remove any existing cached publication channel since connection is changing - m_clientPublicationChannels.TryRemove(clientID, out IServer _); + m_clientPublicationChannels.TryRemove(clientID, out _); // Update payload compression state and strength subscription.UsePayloadCompression = usePayloadCompression; @@ -2956,7 +3002,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff if (connection.Version == 1) { // Send updated signal index cache to client with validated rights of the selected input measurement keys - byte[] serializedSignalIndexCache = SerializeSignalIndexCache(clientID, connection.SignalIndexCache); + byte[]? serializedSignalIndexCache = SerializeSignalIndexCache(clientID, connection.SignalIndexCache); SendClientResponse(clientID, ServerResponse.UpdateSignalIndexCache, ServerCommand.Subscribe, serializedSignalIndexCache); } @@ -2969,9 +3015,9 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff subscription.Start(); // If client has subscribed to any cached measurements, queue them up for the client - if (TryGetAdapterByName(nameof(LatestMeasurementCache), out IActionAdapter cacheAdapter)) + if (TryGetAdapterByName(nameof(LatestMeasurementCache), out IActionAdapter? cacheAdapter)) { - if (cacheAdapter is LatestMeasurementCache cache) + if (cacheAdapter is LatestMeasurementCache cache && subscription.InputMeasurementKeys is not null) { IEnumerable cachedMeasurements = cache.LatestMeasurements.Where(measurement => subscription.InputMeasurementKeys.Any(key => key.SignalID == measurement.ID)); subscription.QueueMeasurementsForProcessing(cachedMeasurements); @@ -3002,9 +3048,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff } else { - message = subscription.InputMeasurementKeys is null ? - $"Client subscribed as {(useCompactMeasurementFormat ? "" : "non-")}compact, but no signals were specified. Make sure \"inputMeasurementKeys\" setting is properly defined." : - $"Client subscribed as {(useCompactMeasurementFormat ? "" : "non-")}compact with {subscription.InputMeasurementKeys.Length} signals."; + message = subscription.InputMeasurementKeys is null ? $"Client subscribed as {(useCompactMeasurementFormat ? "" : "non-")}compact, but no signals were specified. Make sure \"inputMeasurementKeys\" setting is properly defined." : $"Client subscribed as {(useCompactMeasurementFormat ? "" : "non-")}compact with {subscription.InputMeasurementKeys.Length} signals."; } connection.IsSubscribed = true; @@ -3013,9 +3057,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff } else { - message = byteLength > 0 ? - "Not enough buffer was provided to parse client data subscription." : - "Cannot initialize client data subscription without a connection string."; + message = byteLength > 0 ? "Not enough buffer was provided to parse client data subscription." : "Cannot initialize client data subscription without a connection string."; SendClientResponse(clientID, ServerResponse.Failed, ServerCommand.Subscribe, message); OnProcessException(MessageLevel.Warning, new InvalidOperationException(message)); @@ -3065,12 +3107,17 @@ private void HandleUnsubscribeRequest(SubscriberConnection connection) /// Meta-data to be returned to client. protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dictionary> filterExpressions) { +#if NET + using AdoDataConnection adoDatabase = new(ConfigSettings.Default); + DbConnection dbConnection = adoDatabase.Connection; +#else using AdoDataConnection adoDatabase = new("systemSettings"); IDbConnection dbConnection = adoDatabase.Connection; - DataSet metadata = new(); - + // Initialize active node ID - Guid nodeID = Guid.Parse(dbConnection.ExecuteScalar($"SELECT NodeID FROM IaonActionAdapter WHERE ID = {ID}").ToString()); + Guid nodeID = Guid.Parse(dbConnection.ExecuteScalar($"SELECT NodeID FROM IaonActionAdapter WHERE ID = {ID}")?.ToString() ?? Guid.Empty.ToString()); +#endif + DataSet metadata = new(); // Determine whether we're sending internal and external meta-data bool sendExternalMetadata = connection.OperationalModes.HasFlag(OperationalModes.ReceiveExternalMetadata); @@ -3082,11 +3129,37 @@ protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dicti if (string.IsNullOrWhiteSpace(tableExpression)) continue; + string metadataQuery = tableExpression.Trim(); + // Query the table or view information from the database - DataTable table = dbConnection.RetrieveData(adoDatabase.AdapterType, tableExpression); + // ReSharper disable once JoinDeclarationAndInitializer + DataTable table; + Match regexMatch; + + if (adoDatabase.IsSqlite) + { + // SQLite does not support TOP clause, so we need to replace it with LIMIT + regexMatch = Regex.Match(tableExpression, @"\s+TOP\s+(\d+)\s+", RegexOptions.IgnoreCase); + + if (regexMatch.Success) + { + // Remove TOP clause with count + string topCount = regexMatch.Groups[1].Value; + metadataQuery = Regex.Replace(tableExpression, @"\s+TOP\s+\d+\s+", " "); + + // Append LIMIT clause with count + metadataQuery = $"{metadataQuery} LIMIT {topCount}"; + } + } + + #if NET + table = dbConnection.RetrieveData(metadataQuery); + #else + table = dbConnection.RetrieveData(adoDatabase.AdapterType, metadataQuery); + #endif // Remove any expression from table name - Match regexMatch = Regex.Match(tableExpression, @"FROM \w+"); + regexMatch = Regex.Match(metadataQuery, @"FROM \w+"); table.TableName = regexMatch.Value.Split(' ')[1]; string sortField = ""; @@ -3095,8 +3168,10 @@ protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dicti // Build filter list List filters = []; + #if !NET if (table.Columns.Contains("NodeID")) filters.Add($"NodeID = '{nodeID}'"); + #endif if (table.Columns.Contains("Internal") && !(sendInternalMetadata && sendExternalMetadata)) filters.Add($"Internal {(sendExternalMetadata ? "=" : "<>")} 0"); @@ -3104,7 +3179,7 @@ protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dicti if (table.Columns.Contains("OriginalSource") && !(sendInternalMetadata && sendExternalMetadata) && !MutualSubscription) filters.Add($"OriginalSource IS {(sendExternalMetadata ? "NOT" : "")} NULL"); - if (filterExpressions.TryGetValue(table.TableName, out Tuple filterParameters)) + if (filterExpressions.TryGetValue(table.TableName, out Tuple? filterParameters)) { filters.Add($"({filterParameters.Item1})"); sortField = filterParameters.Item2; @@ -3140,7 +3215,7 @@ protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dicti if (filteredRowList.Count == 0) continue; - DataTable metadataTable = metadata.Tables[table.TableName]; + DataTable metadataTable = metadata.Tables[table.TableName]!; // Manually copy-in each row into table foreach (DataRow row in filteredRowList) @@ -3157,41 +3232,41 @@ protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dicti } // Do some post analysis on the meta-data to be delivered to the client, e.g., if a device exists with no associated measurements - don't send the device. - if (!metadata.Tables.Contains("MeasurementDetail") || !metadata.Tables["MeasurementDetail"].Columns.Contains("DeviceAcronym") || !metadata.Tables.Contains("DeviceDetail") || !metadata.Tables["DeviceDetail"].Columns.Contains("Acronym")) + if (!metadata.Tables.Contains("MeasurementDetail") || !metadata.Tables["MeasurementDetail"]!.Columns.Contains("DeviceAcronym") || !metadata.Tables.Contains("DeviceDetail") || !metadata.Tables["DeviceDetail"]!.Columns.Contains("Acronym")) return metadata; List rowsToRemove = []; string deviceAcronym; // Remove device records where no associated measurement records exist - foreach (DataRow row in metadata.Tables["DeviceDetail"].Rows) + foreach (DataRow row in metadata.Tables["DeviceDetail"]!.Rows) { deviceAcronym = row["Acronym"].ToNonNullString(); - if (!string.IsNullOrEmpty(deviceAcronym) && (int)metadata.Tables["MeasurementDetail"].Compute("Count(DeviceAcronym)", $"DeviceAcronym = '{deviceAcronym}'") == 0) + if (!string.IsNullOrEmpty(deviceAcronym) && (int)metadata.Tables["MeasurementDetail"]!.Compute("Count(DeviceAcronym)", $"DeviceAcronym = '{deviceAcronym}'") == 0) rowsToRemove.Add(row); } - if (metadata.Tables.Contains("PhasorDetail") && metadata.Tables["PhasorDetail"].Columns.Contains("DeviceAcronym")) + if (metadata.Tables.Contains("PhasorDetail") && metadata.Tables["PhasorDetail"]!.Columns.Contains("DeviceAcronym")) { // Remove phasor records where no associated device records exist - foreach (DataRow row in metadata.Tables["PhasorDetail"].Rows) + foreach (DataRow row in metadata.Tables["PhasorDetail"]!.Rows) { deviceAcronym = row["DeviceAcronym"].ToNonNullString(); - if (!string.IsNullOrEmpty(deviceAcronym) && (int)metadata.Tables["DeviceDetail"].Compute("Count(Acronym)", $"Acronym = '{deviceAcronym}'") == 0) + if (!string.IsNullOrEmpty(deviceAcronym) && (int)metadata.Tables["DeviceDetail"]!.Compute("Count(Acronym)", $"Acronym = '{deviceAcronym}'") == 0) rowsToRemove.Add(row); } - if (metadata.Tables["PhasorDetail"].Columns.Contains("SourceIndex") && metadata.Tables["MeasurementDetail"].Columns.Contains("PhasorSourceIndex")) + if (metadata.Tables["PhasorDetail"]!.Columns.Contains("SourceIndex") && metadata.Tables["MeasurementDetail"]!.Columns.Contains("PhasorSourceIndex")) { // Remove measurement records where no associated phasor records exist - foreach (DataRow row in metadata.Tables["MeasurementDetail"].Rows) + foreach (DataRow row in metadata.Tables["MeasurementDetail"]!.Rows) { deviceAcronym = row["DeviceAcronym"].ToNonNullString(); int? phasorSourceIndex = row.ConvertField("PhasorSourceIndex"); - if (!string.IsNullOrEmpty(deviceAcronym) && phasorSourceIndex is not null && (int)metadata.Tables["PhasorDetail"].Compute("Count(DeviceAcronym)", $"DeviceAcronym = '{deviceAcronym}' AND SourceIndex = {phasorSourceIndex}") == 0) + if (!string.IsNullOrEmpty(deviceAcronym) && phasorSourceIndex is not null && (int)metadata.Tables["PhasorDetail"]!.Compute("Count(DeviceAcronym)", $"DeviceAcronym = '{deviceAcronym}' AND SourceIndex = {phasorSourceIndex}") == 0) rowsToRemove.Add(row); } } @@ -3250,7 +3325,7 @@ private void HandleMetadataRefresh(SubscriberConnection connection, byte[] buffe try { DataSet metadata = AcquireMetadata(connection, filterExpressions); - byte[] serializedMetadata = SerializeMetadata(clientID, metadata); + byte[]? serializedMetadata = SerializeMetadata(clientID, metadata); long rowCount = metadata.Tables.Cast().Select(dataTable => (long)dataTable.Rows.Count).Sum(); if (rowCount > 0) @@ -3285,7 +3360,7 @@ private void HandleUpdateProcessingInterval(SubscriberConnection connection, byt // Next 4 bytes are an integer representing the new processing interval int processingInterval = BigEndian.ToInt32(buffer, startIndex); - SubscriberAdapter subscription = connection.Subscription; + SubscriberAdapter? subscription = connection.Subscription; if (subscription is null) { @@ -3337,9 +3412,10 @@ private void HandleConfirmNotification(SubscriberConnection connection, byte[] b { lock (m_clientNotificationsLock) { - if (m_clientNotifications.TryGetValue(connection.SubscriberID, out Dictionary notifications)) + if (m_clientNotifications.TryGetValue(connection.SubscriberID, out Dictionary? notifications) /* && notifications is not null */) { - if (notifications.TryGetValue(hash, out string notification)) + // ReSharper disable once CanSimplifyDictionaryRemovingWithSingleCall + if (notifications.TryGetValue(hash, out string? notification)) { notifications.Remove(hash); OnStatusMessage(MessageLevel.Info, $"Subscriber {connection.ConnectionID} confirmed receipt of notification: {notification}."); @@ -3362,18 +3438,18 @@ private void HandleConfirmNotification(SubscriberConnection connection, byte[] b } } - private void HandleConfirmBufferBlock(SubscriberConnection connection, byte[] buffer, int startIndex, int length) + private static void HandleConfirmBufferBlock(SubscriberConnection connection, byte[] buffer, int startIndex, int length) { if (length < 4) return; uint sequenceNumber = BigEndian.ToUInt32(buffer, startIndex); - connection.Subscription.ConfirmBufferBlock(sequenceNumber); + connection.Subscription?.ConfirmBufferBlock(sequenceNumber); } - private void HandleConfirmSignalIndexCache(SubscriberConnection connection) + private static void HandleConfirmSignalIndexCache(SubscriberConnection connection) { - connection.Subscription.ConfirmSignalIndexCache(connection.ClientID); + connection.Subscription?.ConfirmSignalIndexCache(connection.ClientID); } private void HandlePublishCommandMeasurements(SubscriberConnection connection, byte[] buffer, int startIndex) @@ -3394,7 +3470,7 @@ private void HandlePublishCommandMeasurements(SubscriberConnection connection, b CommandMeasurement measurement = new(); connectionStringParser.ParseConnectionString(measurementString, measurement); - measurements.Add(new Measurement() + measurements.Add(new Measurement { Metadata = MeasurementKey.LookUpBySignalID(measurement.SignalID).Metadata, Timestamp = measurement.Timestamp, @@ -3429,12 +3505,12 @@ protected virtual void HandleUserCommand(SubscriberConnection connection, Server OnStatusMessage(MessageLevel.Info, $"Received command code for user-defined command \"{command}\"."); } - private byte[] SerializeSignalIndexCache(Guid clientID, SignalIndexCache signalIndexCache) + private byte[]? SerializeSignalIndexCache(Guid clientID, SignalIndexCache? signalIndexCache) { - if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection connection) || connection is null) + if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection) /* || connection is null */) return null; - if (signalIndexCache.Reference.Count == 0) + if (signalIndexCache is null || signalIndexCache.Reference.Count == 0) return null; OperationalModes operationalModes = connection.OperationalModes; @@ -3443,7 +3519,7 @@ private byte[] SerializeSignalIndexCache(Guid clientID, SignalIndexCache signalI using BlockAllocatedMemoryStream compressedData = new(); if (connection.Version > 1) - compressedData.Write(byte.MaxValue); // Place-holder - actual value updated inside lock + compressedData.WriteByte(byte.MaxValue); // Place-holder - actual value updated inside lock signalIndexCache.Encoding = GetClientEncoding(clientID); byte[] serializedSignalIndexCache = new byte[signalIndexCache.BinaryLength]; @@ -3454,12 +3530,12 @@ private byte[] SerializeSignalIndexCache(Guid clientID, SignalIndexCache signalI { if (connection.Version <= 1) return serializedSignalIndexCache; - + compressedData.Write(serializedSignalIndexCache); return compressedData.ToArray(); } - GZipStream deflater = null; + GZipStream? deflater = null; try { @@ -3479,9 +3555,9 @@ private byte[] SerializeSignalIndexCache(Guid clientID, SignalIndexCache signalI return serializedSignalIndexCache; } - private byte[] SerializeMetadata(Guid clientID, DataSet metadata) + private byte[]? SerializeMetadata(Guid clientID, DataSet metadata) { - if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection connection)) + if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection)) return null; byte[] serializedMetadata; @@ -3489,7 +3565,7 @@ private byte[] SerializeMetadata(Guid clientID, DataSet metadata) OperationalModes operationalModes = connection.OperationalModes; CompressionModes compressionModes = (CompressionModes)(operationalModes & OperationalModes.CompressionModeMask); bool compressMetadata = (operationalModes & OperationalModes.CompressMetadata) > 0; - GZipStream deflater = null; + GZipStream? deflater = null; // Encode XML into encoded data buffer using (BlockAllocatedMemoryStream encodedData = new()) @@ -3557,13 +3633,13 @@ private void ResetMeasurementsPerSecondCounters() m_measurementsPerSecondCount = 0L; } - private void Subscription_BufferBlockRetransmission(object sender, EventArgs eventArgs) + private void Subscription_BufferBlockRetransmission(object? sender, EventArgs eventArgs) { BufferBlockRetransmissions++; } // Bubble up processing complete notifications from subscriptions - private void Subscription_ProcessingComplete(object sender, EventArgs e) + private void Subscription_ProcessingComplete(object? sender, EventArgs e) { // Expose notification via data publisher event subscribers ProcessingComplete?.Invoke(sender, e.Argument2); @@ -3572,15 +3648,14 @@ private void Subscription_ProcessingComplete(object sender, EventArgs e) + private void ServerCommandChannelReceiveClientDataComplete(object? sender, EventArgs e) { try { @@ -3589,7 +3664,7 @@ private void ServerCommandChannelReceiveClientDataComplete(object sender, EventA int length = e.Argument3; int index = 0; - if (length <= 0 || buffer is null) + if (length <= 0) return; byte commandByte = buffer[index]; @@ -3599,7 +3674,7 @@ private void ServerCommandChannelReceiveClientDataComplete(object sender, EventA bool validServerCommand = Enum.TryParse(commandByte.ToString(), out ServerCommand command); // Look up this client connection - if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection connection)) + if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection)) { // Received a request from an unknown client, this request is denied OnStatusMessage(MessageLevel.Warning, $"Ignored {length} byte {(validServerCommand ? command.ToString() : "unidentified")} command request received from an unrecognized client: {clientID}", flags: MessageFlags.UsageIssue); @@ -3675,7 +3750,7 @@ private void ServerCommandChannelReceiveClientDataComplete(object sender, EventA case ServerCommand.UserCommand14: case ServerCommand.UserCommand15: // Handle confirmation of receipt of a user-defined command - HandleUserCommand(connection, command, buffer, index, length); + HandleUserCommand(connection, command, buffer, index, --length); break; } } @@ -3693,7 +3768,7 @@ private void ServerCommandChannelReceiveClientDataComplete(object sender, EventA } } - private void ServerCommandChannelClientConnected(object sender, EventArgs e) + private void ServerCommandChannelClientConnected(object? sender, EventArgs e) { Guid clientID = e.Argument; @@ -3711,7 +3786,7 @@ private void ServerCommandChannelClientConnected(object sender, EventArgs ClientConnections[clientID] = connection; - OnStatusMessage(MessageLevel.Info, $"Client connected to command channel {(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}."); + OnStatusMessage(MessageLevel.Info, $"Client connected to command channel{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}."); if (connection.Authenticated) { @@ -3731,7 +3806,7 @@ private void ServerCommandChannelClientConnected(object sender, EventArgs } } - private void ServerCommandChannelClientDisconnected(object sender, EventArgs e) + private void ServerCommandChannelClientDisconnected(object? sender, EventArgs e) { try { @@ -3744,18 +3819,18 @@ private void ServerCommandChannelClientDisconnected(object sender, EventArgs e) + private void ServerCommandChannelClientConnectingException(object? sender, EventArgs e) { Exception ex = e.Argument; OnProcessException(MessageLevel.Info, new ConnectionException($"Data publisher encountered an exception while connecting client to the command channel: {ex.Message}", ex)); } - private void ServerCommandChannelServerStarted(object sender, EventArgs e) + private void ServerCommandChannelServerStarted(object? sender, EventArgs e) { OnStatusMessage(MessageLevel.Info, "Data publisher command channel started."); } - private void ServerCommandChannelServerStopped(object sender, EventArgs e) + private void ServerCommandChannelServerStopped(object? sender, EventArgs e) { if (Enabled) { @@ -3765,7 +3840,7 @@ private void ServerCommandChannelServerStopped(object sender, EventArgs e) { try { - m_serverCommandChannel.Start(); + m_serverCommandChannel?.Start(); } catch (Exception ex) { @@ -3782,7 +3857,7 @@ private void ServerCommandChannelServerStopped(object sender, EventArgs e) } } - private void ServerCommandChannelSendClientDataException(object sender, EventArgs e) + private void ServerCommandChannelSendClientDataException(object? sender, EventArgs e) { Exception ex = e.Argument2; @@ -3790,7 +3865,7 @@ private void ServerCommandChannelSendClientDataException(object sender, EventArg OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data publisher encountered an exception while sending command channel data to client connection: {ex.Message}", ex)); } - private void ServerCommandChannelReceiveClientDataException(object sender, EventArgs e) + private void ServerCommandChannelReceiveClientDataException(object? sender, EventArgs e) { Exception ex = e.Argument2; @@ -3802,7 +3877,7 @@ private void ServerCommandChannelReceiveClientDataException(object sender, Event #region [ Client Command Channel Event Handlers ] - private void ClientCommandChannelConnectionEstablished(object sender, EventArgs e) + private void ClientCommandChannelConnectionEstablished(object? sender, EventArgs e) { try { @@ -3810,18 +3885,18 @@ private void ClientCommandChannelConnectionEstablished(object sender, EventArgs if (m_proxyClientID is not null && ClientConnections.ContainsKey(m_proxyClientID.GetValueOrDefault())) return; - OnStatusMessage(MessageLevel.Info, $"Client-based command channel subscriber connection established via {m_clientCommandChannel.ServerUri}."); + OnStatusMessage(MessageLevel.Info, $"Client-based command channel subscriber connection established{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}."); m_proxyClientID ??= Guid.NewGuid(); ServerCommandChannelClientConnected(sender, new EventArgs(m_proxyClientID.GetValueOrDefault())); } catch (Exception ex) { - OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to establish client connection session via {m_clientCommandChannel.ServerUri}: {ex.Message}", ex)); + OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to establish client connection session{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}: {ex.Message}", ex)); } } - private void ClientCommandChannelConnectionTerminated(object sender, EventArgs e) + private void ClientCommandChannelConnectionTerminated(object? sender, EventArgs e) { ServerCommandChannelClientDisconnected(sender, new EventArgs(m_proxyClientID.GetValueOrDefault())); @@ -3830,57 +3905,57 @@ private void ClientCommandChannelConnectionTerminated(object sender, EventArgs e // If user didn't initiate disconnect, restart the connection new Action(() => - { - if (!Enabled || m_clientCommandChannel is null || m_clientCommandChannel.CurrentState != ClientState.Disconnected) - return; + { + if (!Enabled || m_clientCommandChannel is null || m_clientCommandChannel.CurrentState != ClientState.Disconnected) + return; - OnStatusMessage(MessageLevel.Info, $"Attempting to re-establish client-based command channel subscriber connection via {m_clientCommandChannel.ServerUri}..."); - m_commandChannelConnectionAttempts = 0; - m_clientCommandChannel.ConnectAsync(); - }) - .DelayAndExecute(1000); + OnStatusMessage(MessageLevel.Info, $"Attempting to re-establish client-based command channel subscriber connection{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}..."); + m_commandChannelConnectionAttempts = 0; + m_clientCommandChannel?.ConnectAsync(); + }) + .DelayAndExecute(1000); } - private void ClientCommandChannelConnectionException(object sender, EventArgs e) + private void ClientCommandChannelConnectionException(object? sender, EventArgs e) { Exception ex = e.Argument; - OnProcessException(MessageLevel.Info, new ConnectionException($"Data publisher encountered an exception while attempting client-based command channel subscriber connection via {m_clientCommandChannel.ServerUri}: {ex.Message}", ex)); + OnProcessException(MessageLevel.Info, new ConnectionException($"Data publisher encountered an exception while attempting client-based command channel subscriber connection{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}: {ex.Message}", ex)); } - private void ClientCommandChannelConnectionAttempt(object sender, EventArgs e) + private void ClientCommandChannelConnectionAttempt(object? sender, EventArgs e) { // Inject a short delay between multiple connection attempts if (m_commandChannelConnectionAttempts > 0) Thread.Sleep(2000); - OnStatusMessage(MessageLevel.Info, $"Attempting client-based command channel connection to subscriber via {m_clientCommandChannel.ServerUri}..."); + OnStatusMessage(MessageLevel.Info, $"Attempting client-based command channel connection to subscriber{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}..."); m_commandChannelConnectionAttempts++; } - private void ClientCommandChannelSendDataException(object sender, EventArgs e) + private void ClientCommandChannelSendDataException(object? sender, EventArgs e) { Exception ex = e.Argument; if (!HandleSocketException(m_proxyClientID.GetValueOrDefault(), ex) && ex is not ObjectDisposedException) - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data publisher encountered an exception while sending client-based command channel data to subscriber connection via {m_clientCommandChannel.ServerUri}: {ex.Message}", ex)); + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data publisher encountered an exception while sending client-based command channel data to subscriber connection{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}: {ex.Message}", ex)); } - private void ClientCommandChannelReceiveDataComplete(object sender, EventArgs e) + private void ClientCommandChannelReceiveDataComplete(object? sender, EventArgs e) { ServerCommandChannelReceiveClientDataComplete(sender, new EventArgs(m_proxyClientID.GetValueOrDefault(), e.Argument1, e.Argument2)); } - private void ClientCommandChannelReceiveDataException(object sender, EventArgs e) + private void ClientCommandChannelReceiveDataException(object? sender, EventArgs e) { Exception ex = e.Argument; if (!HandleSocketException(m_proxyClientID.GetValueOrDefault(), ex) && ex is not ObjectDisposedException) - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data publisher encountered an exception while receiving client-based command channel data from subscriber connection via {m_clientCommandChannel.ServerUri}: {ex.Message}", ex)); + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data publisher encountered an exception while receiving client-based command channel data from subscriber connection{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}: {ex.Message}", ex)); } #endregion - #endregion +#endregion #region [ Static ] @@ -3900,7 +3975,7 @@ private static List ParseAddressList(string addressList) foreach (string address in splitList) { // Attempt to parse the IP address - if (!IPAddress.TryParse(address.Trim(), out IPAddress ipAddress)) + if (!IPAddress.TryParse(address.Trim(), out IPAddress? ipAddress)) continue; // Add the parsed address to the list @@ -3934,8 +4009,8 @@ private static string GetOperationalModes(SubscriberConnection connection) } else { - description.Append($" {(connection.Subscription.UseCompactMeasurementFormat ? "Compact" : "FullSize")}PayloadData["); - description.AppendLine($"{connection.Subscription.TimestampSize}-byte Timestamps]"); + description.Append($" {(connection.Subscription?.UseCompactMeasurementFormat ?? true ? "Compact" : "FullSize")}PayloadData["); + description.AppendLine($"{connection.Subscription?.TimestampSize ?? 8}-byte Timestamps]"); } if ((operationalModes & OperationalModes.CompressSignalIndexCache) > 0 && gzipEnabled) diff --git a/src/lib/DataSubscriber.cs b/src/lib/sttp.core/DataSubscriber.cs similarity index 84% rename from src/lib/DataSubscriber.cs rename to src/lib/sttp.core/DataSubscriber.cs index 3de75e25..abadf032 100644 --- a/src/lib/DataSubscriber.cs +++ b/src/lib/sttp.core/DataSubscriber.cs @@ -1,4878 +1,5101 @@ -//****************************************************************************************************** -// DataSubscriber.cs - Gbtc -// -// Copyright © 2012, 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://www.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/20/2010 - J. Ritchie Carroll -// Generated original version of source code. -// 02/07/2012 - Mehulbhai Thakkar -// Modified SynchronizeMetadata to filter devices by original source and modified insert query -// to populate OriginalSource value. Added to flag to optionally avoid meta-data synchronization. -// 12/20/2012 - Starlynn Danyelle Gilliam -// Modified Header. -// -//****************************************************************************************************** - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Xml; -using GSF; -using GSF.Collections; -using GSF.Communication; -using GSF.Configuration; -using GSF.Data; -using GSF.Diagnostics; -using GSF.IO; -using GSF.Net.Security; -using GSF.Reflection; -using GSF.Security.Cryptography; -using GSF.Threading; -using GSF.TimeSeries; -using GSF.TimeSeries.Adapters; -using GSF.TimeSeries.Data; -using GSF.TimeSeries.Statistics; -using GSF.TimeSeries.Transport; -using GSF.Units; -using sttp.tssc; -using TcpClient = GSF.Communication.TcpClient; -using UdpClient = GSF.Communication.UdpClient; - -// ReSharper disable BadControlBracesIndent -namespace sttp; - -/// -/// Represents a data subscribing client that will connect to a data publisher for a data subscription. -/// -[Description("STTP Subscriber: client that subscribes to an STTP-style publishing server for a streaming data.")] -[EditorBrowsable(EditorBrowsableState.Advanced)] // Normally defined as an input device protocol -public class DataSubscriber : InputAdapterBase -{ - #region [ Members ] - - // Nested Types - - private class SubscribedDevice : IDevice, IDisposable - { - #region [ Members ] - - // Fields - private long m_dataQualityErrors; - private long m_timeQualityErrors; - private long m_deviceErrors; - private long m_measurementsReceived; - private double m_measurementsExpected; - private long m_measurementsWithError; - private long m_measurementsDefined; - private bool m_disposed; - - #endregion - - #region [ Constructors ] - - public SubscribedDevice(string name) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - StatisticsEngine.Register(this, name, "Device", "PMU"); - } - - /// - /// Releases the unmanaged resources before the object is reclaimed by . - /// - ~SubscribedDevice() - { - Unregister(); - } - - #endregion - - #region [ Properties ] - - public string Name { get; } - - public Guid StatusFlagsID { get; set; } - - public Guid FrequencyID { get; set; } - - public Guid DeltaFrequencyID { get; set; } - - public long DataQualityErrors - { - get => Interlocked.Read(ref m_dataQualityErrors); - set => Interlocked.Exchange(ref m_dataQualityErrors, value); - } - - public long TimeQualityErrors - { - get => Interlocked.Read(ref m_timeQualityErrors); - set => Interlocked.Exchange(ref m_timeQualityErrors, value); - } - - public long DeviceErrors - { - get => Interlocked.Read(ref m_deviceErrors); - set => Interlocked.Exchange(ref m_deviceErrors, value); - } - - public long MeasurementsReceived - { - get => Interlocked.Read(ref m_measurementsReceived); - set => Interlocked.Exchange(ref m_measurementsReceived, value); - } - - public long MeasurementsExpected - { - get => (long)Interlocked.CompareExchange(ref m_measurementsExpected, 0.0D, 0.0D); - set => Interlocked.Exchange(ref m_measurementsExpected, value); - } - - public long MeasurementsWithError - { - get => Interlocked.Read(ref m_measurementsWithError); - set => Interlocked.Exchange(ref m_measurementsWithError, value); - } - - public long MeasurementsDefined - { - get => Interlocked.Read(ref m_measurementsDefined); - set => Interlocked.Exchange(ref m_measurementsDefined, value); - } - - #endregion - - #region [ Methods ] - - public override bool Equals(object obj) - { - return obj is SubscribedDevice subscribedDevice && Name.Equals(subscribedDevice.Name); - } - - public override int GetHashCode() - { - return Name.GetHashCode(); - } - - /// - /// Releases all the resources used by the object. - /// - public void Dispose() - { - Unregister(); - GC.SuppressFinalize(this); - } - - private void Unregister() - { - if (m_disposed) - return; - - try - { - StatisticsEngine.Unregister(this); - } - finally - { - m_disposed = true; // Prevent duplicate dispose. - } - } - - #endregion - } - - /// - /// EventArgs implementation for handling user commands. - /// - public class UserCommandArgs : EventArgs - { - /// - /// Creates a new instance of the class. - /// - /// The code for the user command. - /// The code for the server's response. - /// Buffer containing the message from the server. - /// Index into the buffer used to skip the header. - /// The length of the message in the buffer, including the header. - public UserCommandArgs(ServerCommand command, ServerResponse response, byte[] buffer, int startIndex, int length) - { - Command = command; - Response = response; - Buffer = buffer; - StartIndex = startIndex; - Length = length; - } - - /// - /// Gets the code for the user command. - /// - public ServerCommand Command { get; } - - /// - /// Gets the code for the server's response. - /// - public ServerResponse Response { get; } - - /// - /// Gets the buffer containing the message from the server. - /// - public byte[] Buffer { get; } - - /// - /// Gets the index into the buffer used to skip the header. - /// - public int StartIndex { get; } - - /// - /// Gets the length of the message in the buffer, including the header. - /// - public int Length { get; } - } - - // Constants - - /// - /// Defines default value for property. - /// - public const OperationalModes DefaultOperationalModes = (OperationalModes)((uint)OperationalModes.VersionMask & 2U) | OperationalModes.CompressMetadata | OperationalModes.CompressSignalIndexCache | OperationalModes.ReceiveInternalMetadata; - - /// - /// Defines the default value for the property. - /// - public const int DefaultMetadataSynchronizationTimeout = 0; - - /// - /// Defines the default value for the property. - /// - public const bool DefaultUseTransactionForMetadata = true; - - /// - /// Default value for property. - /// - public const string DefaultLoggingPath = "ConfigurationCache"; - - /// - /// Specifies the default value for the property. - /// - public const int DefaultAllowedParsingExceptions = 10; - - /// - /// Specifies the default value for the property. - /// - public const long DefaultParsingExceptionWindow = 50000000L; // 5 seconds - - private const int EvenKey = 0; // Even key/IV index - private const int OddKey = 1; // Odd key/IV index - private const int KeyIndex = 0; // Index of cipher key component in keyIV array - private const int IVIndex = 1; // Index of initialization vector component in keyIV array - - private const long MissingCacheWarningInterval = 20000000; - - // Events - - /// - /// Occurs when client connection to the data publication server is established. - /// - public event EventHandler ConnectionEstablished; - - /// - /// Occurs when client connection to the data publication server is terminated. - /// - public event EventHandler ConnectionTerminated; - - /// - /// Occurs when client connection to the data publication server has successfully authenticated. - /// - public event EventHandler ConnectionAuthenticated; - - /// - /// Occurs when client receives response from the server. - /// - public event EventHandler> ReceivedServerResponse; - - /// - /// Occurs when client receives message from the server in response to a user command. - /// - public event EventHandler ReceivedUserCommandResponse; - - /// - /// Occurs when client receives requested meta-data transmitted by data publication server. - /// - public event EventHandler> MetaDataReceived; - - /// - /// Occurs when first measurement is transmitted by data publication server. - /// - public event EventHandler> DataStartTime; - - /// - /// Indicates that processing for an input adapter (via temporal session) has completed. - /// - /// - /// This event is expected to only be raised when an input adapter has been designed to process - /// a finite amount of data, e.g., reading a historical range of data during temporal processing. - /// - public new event EventHandler> ProcessingComplete; - - /// - /// Occurs when a notification has been received from the . - /// - public event EventHandler> NotificationReceived; - - /// - /// Occurs when the server has sent a notification that its configuration has changed, this - /// can allow subscriber to request updated meta-data if desired. - /// - public event EventHandler ServerConfigurationChanged; - - /// - /// Occurs when number of parsing exceptions exceed during . - /// - public event EventHandler ExceededParsingExceptionThreshold; - - // Fields - private IClient m_clientCommandChannel; - private IServer m_serverCommandChannel; - private UdpClient m_dataChannel; - private Guid m_activeClientID; - private string m_connectionID; - private SharedTimer m_dataStreamMonitor; - private long m_commandChannelConnectionAttempts; - private long m_dataChannelConnectionAttempts; - private volatile SignalIndexCache m_remoteSignalIndexCache; - private volatile SignalIndexCache[] m_signalIndexCache; - private readonly object m_signalIndexCacheLock; - private volatile int m_cacheIndex; - private volatile long[] m_baseTimeOffsets; - private volatile int m_timeIndex; - private volatile byte[][][] m_keyIVs; - private volatile bool m_subscribed; - private volatile int m_lastBytesReceived; - private DateTime m_lastReceivedAt; - private long m_monitoredBytesReceived; - private long m_lastMissingCacheWarning; - private Guid m_nodeID; - private int m_sttpProtocolID; - private bool m_includeTime; - private bool m_metadataRefreshPending; - private readonly LongSynchronizedOperation m_synchronizeMetadataOperation; - private volatile DataSet m_receivedMetadata; - private DataSet m_synchronizedMetadata; - private DateTime m_lastMetaDataRefreshTime; - private OperationalModes m_operationalModes; - private string m_loggingPath; - private RunTimeLog m_runTimeLog; - private bool m_dataGapRecoveryEnabled; - private DataGapRecoverer m_dataGapRecoverer; - private int m_parsingExceptionCount; - private long m_lastParsingExceptionTime; - - private bool m_supportsTemporalProcessing; - private volatile Dictionary> m_subscribedDevicesLookup; - private volatile List> m_statisticsHelpers; - private readonly LongSynchronizedOperation m_registerStatisticsOperation; - - private readonly List m_bufferBlockCache; - private uint m_expectedBufferBlockSequenceNumber; - - private Ticks m_realTime; - private Ticks m_lastStatisticsHelperUpdate; - private SharedTimer m_subscribedDevicesTimer; - - private long m_totalMeasurementsPerSecond; - private long m_measurementsPerSecondCount; - private long m_measurementsInSecond; - private long m_lastSecondsSinceEpoch; - private long m_lifetimeTotalLatency; - private long m_lifetimeMinimumLatency; - private long m_lifetimeMaximumLatency; - private long m_lifetimeLatencyMeasurements; - - private long m_syncProgressTotalActions; - private long m_syncProgressActionsCount; - private long m_syncProgressLastMessage; - - private bool m_disposed; - - #endregion - - #region [ Constructors ] - - /// - /// Creates a new . - /// - public DataSubscriber() - { - m_registerStatisticsOperation = new LongSynchronizedOperation(HandleDeviceStatisticsRegistration) - { - IsBackground = true - }; - - m_synchronizeMetadataOperation = new LongSynchronizedOperation(SynchronizeMetadata) - { - IsBackground = true - }; - - Encoding = Encoding.Unicode; - m_operationalModes = DefaultOperationalModes; - MetadataSynchronizationTimeout = DefaultMetadataSynchronizationTimeout; - AllowedParsingExceptions = DefaultAllowedParsingExceptions; - ParsingExceptionWindow = DefaultParsingExceptionWindow; - - string loggingPath = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(DefaultLoggingPath)); - - if (Directory.Exists(loggingPath)) - m_loggingPath = loggingPath; - - // Default to not using transactions for meta-data on SQL server (helps avoid deadlocks) - try - { - using AdoDataConnection database = new("systemSettings"); - UseTransactionForMetadata = database.DatabaseType != DatabaseType.SQLServer; - } - catch - { - UseTransactionForMetadata = DefaultUseTransactionForMetadata; - } - - DataLossInterval = 10.0D; - m_bufferBlockCache = []; - UseLocalClockAsRealTime = true; - UseSourcePrefixNames = true; - m_signalIndexCacheLock = new object(); - } - - #endregion - - #region [ Properties ] - - /// - /// Gets or sets the security mode used for communications over the command channel. - /// - public SecurityMode SecurityMode { get; set; } - - /// - /// Gets or sets logging path to be used to be runtime and outage logs of the subscriber which are required for - /// automated data recovery. - /// - /// - /// Leave value blank for default path, i.e., installation folder. Can be a fully qualified path or a path that - /// is relative to the installation folder, e.g., a value of "ConfigurationCache" might resolve to - /// "C:\Program Files\MyTimeSeriesApp\ConfigurationCache\". - /// - public string LoggingPath - { - get => m_loggingPath; - set - { - if (!string.IsNullOrWhiteSpace(value)) - { - string loggingPath = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(value)); - - if (Directory.Exists(loggingPath)) - value = loggingPath; - } - - m_loggingPath = value; - } - } - - /// - /// Gets or sets flag that determines if should attempt to auto-connection to using defined connection settings. - /// - public bool AutoConnect { get; set; } - - /// - /// Gets or sets flag that determines if should - /// automatically request meta-data synchronization and synchronize publisher - /// meta-data with its own database configuration. - /// - public bool AutoSynchronizeMetadata { get; set; } - - /// - /// Gets flag that indicates whether the connection will be persisted - /// even while the adapter is offline in order to synchronize metadata. - /// - public bool PersistConnectionForMetadata => - !AutoStart && AutoSynchronizeMetadata && !this.TemporalConstraintIsDefined(); - - /// - /// Gets or sets flag that determines if child devices associated with a subscription - /// should be prefixed with the subscription name and an exclamation point to ensure - /// device name uniqueness - recommended value is true. - /// - public bool UseSourcePrefixNames { get; set; } - - /// - /// Gets or sets requested meta-data filter expressions to be applied by before meta-data is sent. - /// - /// - /// Multiple meta-data filters, such filters for different data tables, should be separated by a semicolon. Specifying fields in the filter - /// expression that do not exist in the data publisher's current meta-data set could cause filter expressions to not be applied and possibly - /// result in no meta-data being received for the specified data table. - /// - /// - /// FILTER MeasurementDetail WHERE SignalType <> 'STAT'; FILTER PhasorDetail WHERE Phase = '+' - /// - public string MetadataFilters { get; set; } - - /// - /// Gets or sets flag that determines if a subscription is mutual, i.e., bidirectional pub/sub. In this mode one node will - /// be the owner and set Internal = True and the other node will be the renter and set Internal = False. - /// - /// - /// - /// This flag is intended to be used in scenarios where a remote subscriber can add new measurements associated with a - /// source device, e.g., creating new calculated result measurements on a remote machine for load distribution that should - /// get associated with a device on the local machine, thus becoming part of the local measurement set. - /// - /// - /// For best results, both the owner and renter subscriptions should be reduced to needed measurements, i.e., renter should - /// only receive measurements needed for remote calculations and owner should only receive new calculated results. Note that - /// when used with a TLS-style subscription this can be accomplished by using the subscription UI screens that control the - /// measurement subscribed flag. For internal subscriptions, reduction of metadata and subscribed measurements will - /// need to be controlled via connection string with metadataFilters and outputMeasurements, respectively. - /// - /// - /// Setting to true will force to true, - /// to false, and to false. - /// - /// - public bool MutualSubscription { get; set; } - - /// - /// Gets or sets flag that informs publisher if base time-offsets can use millisecond resolution to conserve bandwidth. - /// - [Obsolete("SubscriptionInfo object defines this parameter.", false)] - public bool UseMillisecondResolution { get; set; } - - /// - /// Gets or sets flag that determines if a should be used for command channel connections. - /// - public bool UseSimpleTcpClient { get; set; } - - /// - /// Gets flag that determines whether the command channel is connected. - /// - public bool CommandChannelConnected => m_clientCommandChannel?.Enabled ?? m_serverCommandChannel?.Enabled ?? false; - - /// - /// Gets total data packet bytes received during this session. - /// - public long TotalBytesReceived { get; private set; } - - /// - /// Gets or sets data loss monitoring interval, in seconds. Set to zero to disable monitoring. - /// - public double DataLossInterval - { - get => m_dataStreamMonitor?.Interval / 1000.0D ?? 0.0D; - set - { - if (value > 0.0D) - { - if (m_dataStreamMonitor is null) - { - // Create data stream monitoring timer - m_dataStreamMonitor = Common.TimerScheduler.CreateTimer(); - m_dataStreamMonitor.Elapsed += DataStreamMonitor_Elapsed; - m_dataStreamMonitor.AutoReset = true; - m_dataStreamMonitor.Enabled = false; - } - - // Set user specified interval - m_dataStreamMonitor.Interval = (int)(value * 1000.0D); - } - else - { - // Disable data monitor - if (m_dataStreamMonitor is not null) - { - m_dataStreamMonitor.Elapsed -= DataStreamMonitor_Elapsed; - m_dataStreamMonitor.Dispose(); - } - - m_dataStreamMonitor = null; - } - } - } - - /// - /// Gets or sets a set of flags that define ways in which the subscriber and publisher communicate. - /// - public OperationalModes OperationalModes - { - get => m_operationalModes; - set - { - m_operationalModes = value; - OperationalEncoding operationalEncoding = (OperationalEncoding)(value & OperationalModes.EncodingMask); - Encoding = GetCharacterEncoding(operationalEncoding); - } - } - - /// - /// Gets or sets the operational mode flag to compress meta-data. - /// - public bool CompressMetadata - { - get => m_operationalModes.HasFlag(OperationalModes.CompressMetadata); - set - { - if (value) - m_operationalModes |= OperationalModes.CompressMetadata; - else - m_operationalModes &= ~OperationalModes.CompressMetadata; - } - } - - /// - /// Gets or sets the operational mode flag to compress the signal index cache. - /// - public bool CompressSignalIndexCache - { - get => m_operationalModes.HasFlag(OperationalModes.CompressSignalIndexCache); - set - { - if (value) - m_operationalModes |= OperationalModes.CompressSignalIndexCache; - else - m_operationalModes &= ~OperationalModes.CompressSignalIndexCache; - } - } - - /// - /// Gets or sets the operational mode flag to compress data payloads. - /// - public bool CompressPayload - { - get => m_operationalModes.HasFlag(OperationalModes.CompressPayloadData); - set - { - if (value) - m_operationalModes |= OperationalModes.CompressPayloadData; - else - m_operationalModes &= ~OperationalModes.CompressPayloadData; - } - } - - /// - /// Gets or sets the operational mode flag to receive internal meta-data. - /// - public bool ReceiveInternalMetadata - { - get => m_operationalModes.HasFlag(OperationalModes.ReceiveInternalMetadata); - set - { - if (value) - m_operationalModes |= OperationalModes.ReceiveInternalMetadata; - else - m_operationalModes &= ~OperationalModes.ReceiveInternalMetadata; - } - } - - /// - /// Gets or sets flag that determines if measurements for this data subscription should be marked as "internal", i.e., owned and allowed for proxy. - /// - public bool Internal { get; set; } - - /// - /// Gets or sets flag that determines if output measurements should be automatically filtered to only those belonging to the subscriber. - /// - public bool FilterOutputMeasurements { get; set; } - - /// - /// Gets or sets flag that determines if identity inserts should be used for SQL Server connections during meta-data synchronization. - /// - public bool UseIdentityInsertsForMetadata { get; set; } - - /// - /// Gets or sets flag that determines if CALC signals not defined in metadata should be deleted during synchronizations. Do not set this - /// value to true if local calculations are being created, and associated with, data arriving from STTP stream. - /// - public bool AutoDeleteCalculatedMeasurements { get; set; } - - /// - /// Gets or sets flag that determines if ALARM signals not defined in metadata should be deleted during synchronizations. Do not set this - /// value to true if local alarms are being created, and associated with, data arriving from STTP stream. - /// - public bool AutoDeleteAlarmMeasurements { get; set; } - - /// - /// Gets or sets flag that determines if the data subscriber should attempt to synchronize device metadata as independent devices, i.e., - /// not as children of the parent STTP device connection. - /// Defaults to false. - /// - /// - /// This is useful when using an STTP connection to only synchronize metadata from a publisher, but not to receive data. When enabled, - /// the device enabled state will not be synchronized upon creation unless is set to - /// true. In this mode it may be useful to add the original "ConnectionString" field to the publisher's device metadata so it can - /// be synchronized to the subscriber. To ensure no data is received, the subscriber should be configured with an "OutputMeasurements" - /// filter in the adapter's connection string that does not include any measurements, e.g.: - /// outputMeasurements={FILTER ActiveMeasurements WHERE False} - /// - public bool SyncIndependentDevices { get; set; } - - /// - /// Gets or sets flag that determines if the data subscriber should automatically enable independently synced devices. - /// Defaults to false. - /// - public bool AutoEnableIndependentlySyncedDevices { get; set; } - - /// - /// Gets or sets flag that determines if statistics engine should be enabled for the data subscriber. - /// - public bool BypassStatistics { get; set; } - - /// - /// Gets or sets the operational mode flag to receive external meta-data. - /// - public bool ReceiveExternalMetadata - { - get => m_operationalModes.HasFlag(OperationalModes.ReceiveExternalMetadata); - set - { - if (value) - m_operationalModes |= OperationalModes.ReceiveExternalMetadata; - else - m_operationalModes &= ~OperationalModes.ReceiveExternalMetadata; - } - } - - /// - /// Gets or sets the used by the subscriber and publisher. - /// - public OperationalEncoding OperationalEncoding - { - get => (OperationalEncoding)(m_operationalModes & OperationalModes.EncodingMask); - set - { - m_operationalModes &= ~OperationalModes.EncodingMask; - m_operationalModes |= (OperationalModes)value; - Encoding = GetCharacterEncoding(value); - } - } - - /// - /// Gets or sets the used by the subscriber and publisher. - /// - public CompressionModes CompressionModes - { - get => (CompressionModes)(m_operationalModes & OperationalModes.CompressionModeMask); - set - { - m_operationalModes &= ~OperationalModes.CompressionModeMask; - m_operationalModes |= (OperationalModes)value; - - if (value.HasFlag(CompressionModes.TSSC)) - CompressPayload = true; - } - } - - /// - /// Gets or sets the version number of the protocol in use by this subscriber. - /// - public int Version - { - get => (int)(m_operationalModes & OperationalModes.VersionMask); - set - { - m_operationalModes &= ~OperationalModes.VersionMask; - m_operationalModes |= (OperationalModes)value; - } - } - - /// - /// Gets the character encoding defined by the - /// of the communications stream. - /// - public Encoding Encoding { get; private set; } - - /// - /// Gets flag indicating if this adapter supports real-time processing. - /// - /// - /// Setting this value to false indicates that the adapter should not be enabled unless it exists within a temporal session. - /// As an example, this flag can be used in a gateway system to set up two separate subscribers: one to the PDC for real-time - /// data streams and one to the historian for historical data streams. In this scenario, the assumption is that the PDC is - /// the data source for the historian, implying that only local data is destined for archival. - /// - public bool SupportsRealTimeProcessing { get; private set; } - - /// - /// Gets the flag indicating if this adapter supports temporal processing. - /// - /// - /// - /// Although the data subscriber provisions support for temporal processing by receiving historical data from a remote source, - /// the adapter opens sockets and does not need to be engaged within an actual temporal , therefore - /// this method normally returns false to make sure the adapter doesn't get instantiated within a temporal session. - /// - /// - /// Setting this to true means that a subscriber will be initialized within a temporal session to provide historical - /// data from a remote source - this should only be enabled in cases where (1) there is no locally defined, e.g., in-process, - /// historian that can already provide historical data for temporal sessions, and (2) a remote subscriber should be allowed - /// to proxy temporal requests, e.g., those requested for data gap recovery, to an up-stream subscription. This is useful in - /// cases where a primary data subscriber that has data gap recovery enabled can also allow a remote subscription to proxy in - /// data gap recovery requests. It is recommended that remote data gap recovery request parameters be (1) either slightly - /// looser than those of local system to reduce the possibility of duplicated recovery sessions for the same data loss, or - /// (2) only enabled in the end-most system that most needs the recovered data, like a historian. - /// - /// - public override bool SupportsTemporalProcessing => m_supportsTemporalProcessing; - - /// - /// Gets or sets the desired processing interval, in milliseconds, for the adapter. - /// - /// - /// Except for the values of -1 and 0, this value specifies the desired processing interval for data, i.e., basically a delay, - /// or timer interval, over which to process data. A value of -1 means to use the default processing interval while a value of - /// 0 means to process data as fast as possible. - /// - public override int ProcessingInterval - { - get => base.ProcessingInterval; - set - { - base.ProcessingInterval = value; - - // Request server update the processing interval - SendServerCommand(ServerCommand.UpdateProcessingInterval, BigEndian.GetBytes(value)); - } - } - - /// - /// Gets or sets the flag that determines whether to request that the subscription be throttled to certain publication interval, see . - /// - public bool Throttled { get; set; } - - /// - /// Gets or sets the interval, in seconds, at which data should be published when using a throttled subscription. - /// - public double PublishInterval { get; set; } = -1.0D; - - /// - /// Gets or sets the timeout used when executing database queries during meta-data synchronization. - /// - public int MetadataSynchronizationTimeout { get; set; } - - /// - /// Gets or sets flag that determines if meta-data synchronization should be performed within a transaction. - /// - public bool UseTransactionForMetadata { get; set; } - - /// - /// Gets or sets flag that determines whether to use the local clock when calculating statistics. - /// - public bool UseLocalClockAsRealTime { get; set; } - - /// - /// Gets or sets number of parsing exceptions allowed during before connection is reset. - /// - public int AllowedParsingExceptions { get; set; } - - /// - /// Gets or sets time duration, in , to monitor parsing exceptions. - /// - public Ticks ParsingExceptionWindow { get; set; } - - /// - /// Gets or sets based data source available to this . - /// - public override DataSet DataSource - { - get => base.DataSource; - set - { - base.DataSource = value; - m_registerStatisticsOperation.RunOnce(); - - bool outputMeasurementsUpdated = AutoConnect && UpdateOutputMeasurements(); - - // For automatic connections, when meta-data refresh is complete, update output measurements to see if any - // points for subscription have changed after re-application of filter expressions and if so, resubscribe - if (outputMeasurementsUpdated && Enabled && CommandChannelConnected) - { - OnStatusMessage(MessageLevel.Info, "Meta-data received from publisher modified measurement availability, adjusting active subscription..."); - - // Updating subscription will restart data stream monitor upon successful resubscribe - if (AutoStart) - SubscribeToOutputMeasurements(true); - } - - if (m_dataGapRecoverer is not null) - m_dataGapRecoverer.DataSource = value; - } - } - - /// - /// Gets or sets output measurement keys that are requested by other adapters based on what adapter says it can provide. - /// - public override MeasurementKey[] RequestedOutputMeasurementKeys - { - get => base.RequestedOutputMeasurementKeys; - set - { - MeasurementKey[] oldKeys = base.RequestedOutputMeasurementKeys ?? []; - MeasurementKey[] newKeys = value ?? []; - HashSet oldKeySet = [..oldKeys]; - - base.RequestedOutputMeasurementKeys = value; - - if (AutoStart || !Enabled || !CommandChannelConnected || oldKeySet.SetEquals(newKeys)) - return; - - OnStatusMessage(MessageLevel.Info, "Requested measurements have changed, adjusting active subscription..."); - SubscribeToOutputMeasurements(true); - } - } - - /// - /// Gets or sets output measurements that the will produce, if any. - /// - public override IMeasurement[] OutputMeasurements - { - get => base.OutputMeasurements; - - set - { - base.OutputMeasurements = value; - - if (m_dataGapRecoverer is not null) - m_dataGapRecoverer.FilterExpression = this.OutputMeasurementKeys().Select(key => key.SignalID.ToString()).ToDelimitedString(';'); - } - } - - /// - /// Gets connection info for adapter, if any. - /// - public override string ConnectionInfo - { - get - { - if (m_serverCommandChannel is not null && string.IsNullOrWhiteSpace(m_connectionID)) - { - Guid clientID = m_serverCommandChannel.ClientIDs.FirstOrDefault(); - IPEndPoint endPoint = GetCommandChannelSocket()?.RemoteEndPoint as IPEndPoint; - m_connectionID = SubscriberConnection.GetEndPointConnectionID(clientID, endPoint); - } - - string commandChannelServerUri = m_clientCommandChannel?.ServerUri ?? m_connectionID; - string dataChannelServerUri = m_dataChannel?.ServerUri; - - if (string.IsNullOrWhiteSpace(commandChannelServerUri) && string.IsNullOrWhiteSpace(dataChannelServerUri)) - return null; - - if (string.IsNullOrWhiteSpace(dataChannelServerUri)) - return commandChannelServerUri; - - if (string.IsNullOrWhiteSpace(commandChannelServerUri)) - return dataChannelServerUri; - - return $"{commandChannelServerUri} / {dataChannelServerUri}"; - } - } - - /// - /// Gets the status of this . - /// - /// - /// Derived classes should provide current status information about the adapter for display purposes. - /// - public override string Status - { - get - { - StringBuilder status = new(); - - status.AppendLine($" Protocol version: {Version}"); - status.AppendLine($" Connected: {CommandChannelConnected}"); - status.AppendLine($" Subscribed: {m_subscribed}"); - status.AppendLine($" Security mode: {SecurityMode}"); - status.AppendLine($" Authenticated: {SecurityMode == SecurityMode.TLS && CommandChannelConnected}"); - status.AppendLine($" Compression modes: {CompressionModes}"); - status.AppendLine($" Mutual subscription: {MutualSubscription}{(MutualSubscription ? $" - System has {(Internal ? "Owner" : "Renter")} Role" : "")}"); - status.AppendLine($" Mark received as internal: {Internal}"); - status.AppendLine($" Receive internal metadata: {ReceiveInternalMetadata}"); - status.AppendLine($" Receive external metadata: {ReceiveExternalMetadata}"); - status.AppendLine($"Filter output measurements: {FilterOutputMeasurements}"); - status.AppendLine($" Synchronize metadata IDs: {UseIdentityInsertsForMetadata}"); - status.AppendLine($" Auto delete CALC signals: {AutoDeleteCalculatedMeasurements}"); - status.AppendLine($" Auto delete ALRM signals: {AutoDeleteAlarmMeasurements}"); - status.AppendLine($" Sync independent devices: {SyncIndependentDevices}"); - - if (SyncIndependentDevices) - status.AppendLine($"Independent synced devices: {(AutoEnableIndependentlySyncedDevices? "enabled" : "disabled")} on creation"); - - status.AppendLine($" Bypass statistics engine: {BypassStatistics}"); - status.AppendLine($" Total bytes received: {TotalBytesReceived:N0}"); - status.AppendLine($" Data packet security: {(SecurityMode == SecurityMode.TLS && m_dataChannel is null ? "Secured via TLS" : m_keyIVs is null ? "Unencrypted" : "AES Encrypted")}"); - status.AppendLine($" Data monitor enabled: {m_dataStreamMonitor is not null && m_dataStreamMonitor.Enabled}"); - status.AppendLine($" Logging path: {FilePath.TrimFileName(m_loggingPath.ToNonNullNorWhiteSpace(FilePath.GetAbsolutePath("")), 51)}"); - status.AppendLine($"No data reconnect interval: {(DataLossInterval > 0.0D ? $"{DataLossInterval:0.000} seconds" : "Disabled")}"); - status.AppendLine($" Data gap recovery mode: {(m_dataGapRecoveryEnabled ? "Enabled" : "Disabled")}"); - - if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null) - status.Append(m_dataGapRecoverer.Status); - - if (m_runTimeLog is not null) - { - status.AppendLine(); - status.AppendLine("Run-Time Log Status".CenterText(50)); - status.AppendLine("-------------------".CenterText(50)); - status.Append(m_runTimeLog.Status); - } - - if (m_dataChannel is not null) - { - status.AppendLine(); - status.AppendLine("Data Channel Status".CenterText(50)); - status.AppendLine("-------------------".CenterText(50)); - status.Append(m_dataChannel.Status); - } - - if (m_clientCommandChannel is not null) - { - status.AppendLine(); - status.AppendLine("Command Channel Status".CenterText(50)); - status.AppendLine("----------------------".CenterText(50)); - status.Append(m_clientCommandChannel.Status); - - status.AppendLine($" Using simple TCP client: {UseSimpleTcpClient}"); - } - - if (m_serverCommandChannel is not null) - { - status.AppendLine(); - status.AppendLine("Command Channel Status".CenterText(50)); - status.AppendLine("----------------------".CenterText(50)); - status.Append(m_serverCommandChannel.Status); - } - - status.Append(base.Status); - - return status.ToString(); - } - } - - /// - /// Gets a flag that determines if this uses an asynchronous connection. - /// - protected override bool UseAsyncConnect => true; - - /// - /// Gets or sets reference to data channel, attaching and/or detaching to events as needed. - /// - protected UdpClient DataChannel - { - get => m_dataChannel; - set - { - if (m_dataChannel is not null) - { - // Detach from events on existing data channel reference - m_dataChannel.ConnectionException -= DataChannelConnectionException; - m_dataChannel.ConnectionAttempt -= DataChannelConnectionAttempt; - m_dataChannel.ReceiveData -= DataChannelReceiveData; - m_dataChannel.ReceiveDataException -= DataChannelReceiveDataException; - - if (m_dataChannel != value) - m_dataChannel.Dispose(); - } - - // Assign new data channel reference - m_dataChannel = value; - - if (m_dataChannel is not null) - { - // Attach to desired events on new data channel reference - m_dataChannel.ConnectionException += DataChannelConnectionException; - m_dataChannel.ConnectionAttempt += DataChannelConnectionAttempt; - m_dataChannel.ReceiveData += DataChannelReceiveData; - m_dataChannel.ReceiveDataException += DataChannelReceiveDataException; - } - } - } - - /// - /// Gets or sets reference to command channel, attaching and/or detaching to events as needed. - /// - protected IClient ClientCommandChannel - { - get => m_clientCommandChannel; - set - { - if (m_clientCommandChannel is not null) - { - // Detach from events on existing command channel reference - m_clientCommandChannel.ConnectionAttempt -= ClientCommandChannelConnectionAttempt; - m_clientCommandChannel.ConnectionEstablished -= ClientCommandChannelConnectionEstablished; - m_clientCommandChannel.ConnectionException -= ClientCommandChannelConnectionException; - m_clientCommandChannel.ConnectionTerminated -= ClientCommandChannelConnectionTerminated; - m_clientCommandChannel.ReceiveData -= ClientCommandChannelReceiveData; - m_clientCommandChannel.ReceiveDataException -= ClientCommandChannelReceiveDataException; - m_clientCommandChannel.SendDataException -= ClientCommandChannelSendDataException; - - if (m_clientCommandChannel != value) - m_clientCommandChannel.Dispose(); - } - - // Assign new command channel reference - m_clientCommandChannel = value; - - if (m_clientCommandChannel is not null) - { - // Attach to desired events on new command channel reference - m_clientCommandChannel.ConnectionAttempt += ClientCommandChannelConnectionAttempt; - m_clientCommandChannel.ConnectionEstablished += ClientCommandChannelConnectionEstablished; - m_clientCommandChannel.ConnectionException += ClientCommandChannelConnectionException; - m_clientCommandChannel.ConnectionTerminated += ClientCommandChannelConnectionTerminated; - m_clientCommandChannel.ReceiveData += ClientCommandChannelReceiveData; - m_clientCommandChannel.ReceiveDataException += ClientCommandChannelReceiveDataException; - m_clientCommandChannel.SendDataException += ClientCommandChannelSendDataException; - } - } - } - - /// - /// Gets or sets reference to command channel, attaching and/or detaching to events as needed. - /// - /// - /// This handles reverse connectivity operations. - /// - protected IServer ServerCommandChannel - { - get => m_serverCommandChannel; - set - { - if (m_serverCommandChannel is not null) - { - // Detach from events on existing command channel reference - m_serverCommandChannel.ClientConnected -= ServerCommandChannelClientConnected; - m_serverCommandChannel.ClientDisconnected -= ServerCommandChannelClientDisconnected; - m_serverCommandChannel.ClientConnectingException -= ServerCommandChannelClientConnectingException; - m_serverCommandChannel.ReceiveClientData -= ServerCommandChannelReceiveClientData; - m_serverCommandChannel.ReceiveClientDataException -= ServerCommandChannelReceiveClientDataException; - m_serverCommandChannel.SendClientDataException -= ServerCommandChannelSendClientDataException; - m_serverCommandChannel.ServerStarted -= ServerCommandChannelServerStarted; - m_serverCommandChannel.ServerStopped -= ServerCommandChannelServerStopped; - - if (m_serverCommandChannel != value) - m_serverCommandChannel.Dispose(); - } - - // Assign new command channel reference - m_serverCommandChannel = value; - - if (m_serverCommandChannel is not null) - { - // Attach to desired events on new command channel reference - m_serverCommandChannel.ClientConnected += ServerCommandChannelClientConnected; - m_serverCommandChannel.ClientDisconnected += ServerCommandChannelClientDisconnected; - m_serverCommandChannel.ClientConnectingException += ServerCommandChannelClientConnectingException; - m_serverCommandChannel.ReceiveClientData += ServerCommandChannelReceiveClientData; - m_serverCommandChannel.ReceiveClientDataException += ServerCommandChannelReceiveClientDataException; - m_serverCommandChannel.SendClientDataException += ServerCommandChannelSendClientDataException; - m_serverCommandChannel.ServerStarted += ServerCommandChannelServerStarted; - m_serverCommandChannel.ServerStopped += ServerCommandChannelServerStopped; - } - } - } - - /// - /// Gets the total number of measurements processed through this data publisher over the lifetime of the subscriber. - /// - public long LifetimeMeasurements { get; private set; } - - /// - /// Gets the minimum value of the measurements per second calculation. - /// - public long MinimumMeasurementsPerSecond { get; private set; } - - /// - /// Gets the maximum value of the measurements per second calculation. - /// - public long MaximumMeasurementsPerSecond { get; private set; } - - /// - /// Gets the average value of the measurements per second calculation. - /// - public long AverageMeasurementsPerSecond => m_measurementsPerSecondCount == 0L ? 0L : m_totalMeasurementsPerSecond / m_measurementsPerSecondCount; - - /// - /// Gets the minimum latency calculated over the full lifetime of the subscriber. - /// - public int LifetimeMinimumLatency => (int)Ticks.ToMilliseconds(m_lifetimeMinimumLatency); - - /// - /// Gets the maximum latency calculated over the full lifetime of the subscriber. - /// - public int LifetimeMaximumLatency => (int)Ticks.ToMilliseconds(m_lifetimeMaximumLatency); - - /// - /// Gets the average latency calculated over the full lifetime of the subscriber. - /// - public int LifetimeAverageLatency => m_lifetimeLatencyMeasurements == 0 ? -1 : (int)Ticks.ToMilliseconds(m_lifetimeTotalLatency / m_lifetimeLatencyMeasurements); - - /// - /// Gets real-time as determined by either the local clock or the latest measurement received. - /// - protected Ticks RealTime => UseLocalClockAsRealTime ? DateTime.UtcNow.Ticks : m_realTime; - - #endregion - - #region [ Methods ] - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected override void Dispose(bool disposing) - { - if (m_disposed) - return; - - try - { - if (!disposing) - return; - - DataLossInterval = 0.0D; - ClientCommandChannel = null; - ServerCommandChannel = null; - DataChannel = null; - - if (m_dataGapRecoverer is not null) - { - m_dataGapRecoverer.RecoveredMeasurements -= DataGapRecoverer_RecoveredMeasurements; - m_dataGapRecoverer.StatusMessage -= DataGapRecoverer_StatusMessage; - m_dataGapRecoverer.ProcessException -= DataGapRecoverer_ProcessException; - m_dataGapRecoverer.Dispose(); - m_dataGapRecoverer = null; - } - - if (m_runTimeLog is not null) - { - m_runTimeLog.ProcessException -= RunTimeLog_ProcessException; - m_runTimeLog.Dispose(); - m_runTimeLog = null; - } - - if (m_subscribedDevicesTimer is not null) - { - m_subscribedDevicesTimer.Elapsed -= SubscribedDevicesTimer_Elapsed; - m_subscribedDevicesTimer.Dispose(); - m_subscribedDevicesTimer = null; - } - } - finally - { - m_disposed = true; // Prevent duplicate dispose. - base.Dispose(disposing); // Call base class Dispose(). - } - } - - /// - /// Initializes . - /// - public override void Initialize() - { - base.Initialize(); - - Dictionary settings = Settings; - - // See if user has opted for different operational modes - if (settings.TryGetValue(nameof(OperationalModes), out string setting) && Enum.TryParse(setting, true, out OperationalModes operationalModes)) - OperationalModes = operationalModes; - - // Set the security mode if explicitly defined - if (!settings.TryGetValue(nameof(SecurityMode), out setting) || !Enum.TryParse(setting, true, out SecurityMode securityMode)) - securityMode = SecurityMode.None; - - SecurityMode = securityMode; - - // Apply any version override (e.g., to downgrade to older version) - if (settings.TryGetValue(nameof(Version), out setting) && int.TryParse(setting, out int value) && value < 32) - Version = value; - - // Apply gateway compression mode to operational mode flags - if (settings.TryGetValue(nameof(CompressionModes), out setting) && Enum.TryParse(setting, true, out CompressionModes compressionModes)) - CompressionModes = compressionModes; - - // Check if output measurements should be filtered to only those belonging to the subscriber - FilterOutputMeasurements = !settings.TryGetValue(nameof(FilterOutputMeasurements), out setting) || setting.ParseBoolean(); - - // Check if the subscriber supports real-time and historical processing - SupportsRealTimeProcessing = !settings.TryGetValue(nameof(SupportsRealTimeProcessing), out setting) || setting.ParseBoolean(); - m_supportsTemporalProcessing = settings.TryGetValue(nameof(SupportsTemporalProcessing), out setting) && setting.ParseBoolean(); - - // Check if measurements for this connection should be marked as "internal" - i.e., owned and allowed for proxy - if (settings.TryGetValue(nameof(Internal), out setting)) - Internal = setting.ParseBoolean(); - - // Check if user has explicitly defined the ReceiveInternalMetadata flag - if (settings.TryGetValue(nameof(ReceiveInternalMetadata), out setting)) - ReceiveInternalMetadata = setting.ParseBoolean(); - - // Check if user has explicitly defined the ReceiveExternalMetadata flag - if (settings.TryGetValue(nameof(ReceiveExternalMetadata), out setting)) - ReceiveExternalMetadata = setting.ParseBoolean(); - - // Check if user has explicitly defined the MutualSubscription flag - if (settings.TryGetValue(nameof(MutualSubscription), out setting) && setting.ParseBoolean()) - { - MutualSubscription = true; - ReceiveInternalMetadata = true; - ReceiveExternalMetadata = false; - FilterOutputMeasurements = false; - } - - // Check if user has defined a meta-data synchronization timeout - if (settings.TryGetValue(nameof(MetadataSynchronizationTimeout), out setting) && int.TryParse(setting, out int metadataSynchronizationTimeout)) - MetadataSynchronizationTimeout = metadataSynchronizationTimeout; - - // Check if user has defined a flag for using a transaction during meta-data synchronization - if (settings.TryGetValue(nameof(UseTransactionForMetadata), out setting)) - UseTransactionForMetadata = setting.ParseBoolean(); - - // Check if user has defined a flag for using identity inserts during meta-data synchronization - if (settings.TryGetValue(nameof(UseIdentityInsertsForMetadata), out setting)) - UseIdentityInsertsForMetadata = setting.ParseBoolean(); - - // Check if user has defined a flag for auto-deletion of CALC signals during meta-data synchronization - if (settings.TryGetValue(nameof(AutoDeleteCalculatedMeasurements), out setting)) - AutoDeleteCalculatedMeasurements = setting.ParseBoolean(); - - // Check if user has defined a flag for auto-deletion of ALRM signals during meta-data synchronization - if (settings.TryGetValue(nameof(AutoDeleteAlarmMeasurements), out setting)) - AutoDeleteAlarmMeasurements = setting.ParseBoolean(); - - // Check if user has defined a flag for synchronizing independent devices during meta-data synchronization - if (settings.TryGetValue(nameof(SyncIndependentDevices), out setting)) - SyncIndependentDevices = setting.ParseBoolean(); - - // Check if user has defined a flag for auto-enabling independently synced devices - if (settings.TryGetValue(nameof(AutoEnableIndependentlySyncedDevices), out setting)) - AutoEnableIndependentlySyncedDevices = setting.ParseBoolean(); - - // Check if user wants to request that publisher use millisecond resolution to conserve bandwidth - #pragma warning disable CS0618 // Type or member is obsolete - UseMillisecondResolution = !settings.TryGetValue(nameof(UseMillisecondResolution), out setting) || setting.ParseBoolean(); - #pragma warning restore CS0618 - - // Check if user has defined any meta-data filter expressions - if (settings.TryGetValue(nameof(MetadataFilters), out setting)) - MetadataFilters = setting; - - // Define auto connect setting - if (settings.TryGetValue(nameof(AutoConnect), out setting)) - { - AutoConnect = setting.ParseBoolean(); - - if (AutoConnect) - AutoSynchronizeMetadata = true; - } - - // Define the maximum allowed exceptions before resetting the connection - if (settings.TryGetValue(nameof(AllowedParsingExceptions), out setting)) - AllowedParsingExceptions = int.Parse(setting); - - // Define the window of time over which parsing exceptions are tolerated - if (settings.TryGetValue(nameof(ParsingExceptionWindow), out setting)) - ParsingExceptionWindow = Ticks.FromSeconds(double.Parse(setting)); - - // Check if synchronize meta-data is explicitly enabled or disabled - if (settings.TryGetValue(nameof(AutoSynchronizeMetadata), out setting)) - AutoSynchronizeMetadata = setting.ParseBoolean(); - else if (settings.TryGetValue("synchronizeMetadata", out setting)) - AutoSynchronizeMetadata = setting.ParseBoolean(); - - // Determine if source name prefixes should be applied during metadata synchronization - if (settings.TryGetValue(nameof(UseSourcePrefixNames), out setting)) - UseSourcePrefixNames = setting.ParseBoolean(); - - // Define data loss interval - if (settings.TryGetValue(nameof(DataLossInterval), out setting) && double.TryParse(setting, out double interval)) - DataLossInterval = interval; - - // Define buffer size - if (!settings.TryGetValue("bufferSize", out setting) || !int.TryParse(setting, out int bufferSize)) - bufferSize = DataPublisher.DefaultBufferSize; - - if (settings.TryGetValue(nameof(UseLocalClockAsRealTime), out setting)) - UseLocalClockAsRealTime = setting.ParseBoolean(); - - // Handle throttled subscription options - if (settings.TryGetValue(nameof(Throttled), out setting)) - Throttled = setting.ParseBoolean(); - - if (settings.TryGetValue(nameof(PublishInterval), out setting) && double.TryParse(setting, out interval)) - PublishInterval = interval; - - if (AutoConnect) - { - // Connect to local events when automatically engaging connection cycle - ConnectionAuthenticated += DataSubscriber_ConnectionAuthenticated; - MetaDataReceived += DataSubscriber_MetaDataReceived; - - // Update output measurements to include "subscribed" points - UpdateOutputMeasurements(true); - } - else if (AutoSynchronizeMetadata) - { - // Output measurements do not include "subscribed" points, - // but should still be filtered if applicable - TryFilterOutputMeasurements(); - } - - // Attempt to retrieve any defined command channel settings - Dictionary commandChannelSettings = settings.TryGetValue("commandChannel", out string commandChannelConnectionString) ? commandChannelConnectionString.ParseKeyValuePairs() : settings; - - if (string.IsNullOrWhiteSpace(commandChannelConnectionString)) - commandChannelConnectionString = ConnectionString; - - bool serverBasedConnection = !commandChannelSettings.TryGetValue("server", out string server) || string.IsNullOrWhiteSpace(server); - - if (settings.TryGetValue(nameof(UseSimpleTcpClient), out setting)) - UseSimpleTcpClient = setting.ParseBoolean(); - - if (securityMode == SecurityMode.TLS) - { - bool checkCertificateRevocation; - - if (!commandChannelSettings.TryGetValue("localCertificate", out string localCertificate) || !File.Exists(localCertificate)) - localCertificate = GetLocalCertificate(); - - if (!commandChannelSettings.TryGetValue("remoteCertificate", out string remoteCertificate) || !RemoteCertificateExists(ref remoteCertificate)) - throw new ArgumentException("The \"remoteCertificate\" setting must be defined and certificate file must exist when using TLS security mode."); - - if (!commandChannelSettings.TryGetValue("validPolicyErrors", out setting) || !Enum.TryParse(setting, out SslPolicyErrors validPolicyErrors)) - validPolicyErrors = SslPolicyErrors.None; - - if (!commandChannelSettings.TryGetValue("validChainFlags", out setting) || !Enum.TryParse(setting, out X509ChainStatusFlags validChainFlags)) - validChainFlags = X509ChainStatusFlags.NoError; - - if (commandChannelSettings.TryGetValue("checkCertificateRevocation", out setting) && !string.IsNullOrWhiteSpace(setting)) - checkCertificateRevocation = setting.ParseBoolean(); - else - checkCertificateRevocation = true; - - SimpleCertificateChecker certificateChecker = new(); - - // Set up certificate checker - certificateChecker.TrustedCertificates.Add(new X509Certificate2(FilePath.GetAbsolutePath(remoteCertificate))); - certificateChecker.ValidPolicyErrors = validPolicyErrors; - certificateChecker.ValidChainFlags = validChainFlags; - - if (serverBasedConnection) - { - // Create a new TLS server - TlsServer commandChannel = new() - { - ConfigurationString = commandChannelConnectionString, - PayloadAware = true, - PayloadMarker = null, - PayloadEndianOrder = EndianOrder.BigEndian, - PersistSettings = false, - MaxClientConnections = 1, // Subscriber can only serve a single publisher - CertificateFile = localCertificate, - CheckCertificateRevocation = checkCertificateRevocation, - CertificateChecker = certificateChecker, - RequireClientCertificate = true, - ReceiveBufferSize = bufferSize, - SendBufferSize = bufferSize, - NoDelay = true - }; - - // Assign command channel server reference and attach to needed events - ServerCommandChannel = commandChannel; - } - else - { - // Create a new TLS client - TlsClient commandChannel = new() - { - ConnectionString = commandChannelConnectionString, - PayloadAware = true, - PayloadMarker = null, - PayloadEndianOrder = EndianOrder.BigEndian, - PersistSettings = false, - MaxConnectionAttempts = 1, - CertificateFile = FilePath.GetAbsolutePath(localCertificate), - CheckCertificateRevocation = checkCertificateRevocation, - CertificateChecker = certificateChecker, - ReceiveBufferSize = bufferSize, - SendBufferSize = bufferSize, - NoDelay = true - }; - - // Assign command channel client reference and attach to needed events - ClientCommandChannel = commandChannel; - } - } - else - { - if (serverBasedConnection) - { - // Create a new TCP server - TcpServer commandChannel = new() - { - ConfigurationString = commandChannelConnectionString, - PayloadAware = true, - PayloadMarker = null, - PayloadEndianOrder = EndianOrder.BigEndian, - PersistSettings = false, - MaxClientConnections = 1, // Subscriber can only serve a single publisher - ReceiveBufferSize = bufferSize, - SendBufferSize = bufferSize, - NoDelay = true - }; - - // Assign command channel server reference and attach to needed events - ServerCommandChannel = commandChannel; - } - else - { - if (UseSimpleTcpClient) - { - // Create a new simple TCP client - TcpSimpleClient commandChannel = new() - { - ConnectionString = commandChannelConnectionString, - PayloadAware = true, - PayloadMarker = null, - PayloadEndianOrder = EndianOrder.BigEndian, - PersistSettings = false, - MaxConnectionAttempts = 1, - ReceiveBufferSize = bufferSize, - SendBufferSize = bufferSize, - NoDelay = true - }; - - // Assign command channel client reference and attach to needed events - ClientCommandChannel = commandChannel; - } - else - { - // Create a new TCP client - TcpClient commandChannel = new() - { - ConnectionString = commandChannelConnectionString, - PayloadAware = true, - PayloadMarker = null, - PayloadEndianOrder = EndianOrder.BigEndian, - PersistSettings = false, - MaxConnectionAttempts = 1, - ReceiveBufferSize = bufferSize, - SendBufferSize = bufferSize, - NoDelay = true - }; - - // Assign command channel client reference and attach to needed events - ClientCommandChannel = commandChannel; - } - } - } - - // Check for simplified compression setup flag - if (settings.TryGetValue("compression", out setting) && setting.ParseBoolean()) - { - CompressionModes |= CompressionModes.TSSC | CompressionModes.GZip; - OperationalModes |= OperationalModes.CompressPayloadData | OperationalModes.CompressMetadata | OperationalModes.CompressSignalIndexCache; - } - - // Get logging path, if any has been defined - if (settings.TryGetValue(nameof(LoggingPath), out setting)) - { - setting = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(setting)); - - if (Directory.Exists(setting)) - m_loggingPath = setting; - else - OnStatusMessage(MessageLevel.Info, $"Logging path \"{setting}\" not found, defaulting to \"{FilePath.GetAbsolutePath("")}\"...", flags: MessageFlags.UsageIssue); - } - - // Initialize data gap recovery processing, if requested - if (settings.TryGetValue("dataGapRecovery", out setting)) - { - if (m_clientCommandChannel is null) - { - m_dataGapRecoveryEnabled = false; - OnStatusMessage(MessageLevel.Warning, "Cannot use data gap recovery operations with a server-based data subscriber configuration. Data gap recovery will not be enabled.", "Data Subscriber Initialization", MessageFlags.UsageIssue); - } - else - { - // Make sure setting exists to allow user to by-pass data gap recovery at a configuration level - ConfigurationFile configFile = ConfigurationFile.Current; - CategorizedSettingsElementCollection systemSettings = configFile.Settings["systemSettings"]; - CategorizedSettingsElement dataGapRecoveryEnabledSetting = systemSettings["DataGapRecoveryEnabled"]; - - // See if this node should process phasor source validation - if (dataGapRecoveryEnabledSetting is null || dataGapRecoveryEnabledSetting.ValueAsBoolean()) - { - // Example connection string for data gap recovery: - // dataGapRecovery={enabled=true; recoveryStartDelay=10.0; minimumRecoverySpan=0.0; maximumRecoverySpan=3600.0} - Dictionary dataGapSettings = setting.ParseKeyValuePairs(); - - if (dataGapSettings.TryGetValue("enabled", out setting) && setting.ParseBoolean()) - { - // Remove dataGapRecovery connection setting from command channel connection string, if defined there. - // This will prevent any recursive data gap recovery operations from being established: - Dictionary connectionSettings = m_clientCommandChannel.ConnectionString.ParseKeyValuePairs(); - connectionSettings.Remove("dataGapRecovery"); - connectionSettings.Remove("synchronizeMetadata"); - connectionSettings.Remove(nameof(AutoConnect)); - connectionSettings.Remove(nameof(AutoSynchronizeMetadata)); - connectionSettings.Remove(nameof(OutputMeasurements)); - connectionSettings.Remove(nameof(BypassStatistics)); - connectionSettings.Remove(nameof(LoggingPath)); - - if (dataGapSettings.ContainsKey("server")) - connectionSettings.Remove("server"); - - // Note that the data gap recoverer will connect on the same command channel port as - // the real-time subscriber (TCP only) - m_dataGapRecoveryEnabled = true; - - m_dataGapRecoverer = new DataGapRecoverer - { - SourceConnectionName = Name, - DataSource = DataSource, - ConnectionString = string.Join("; ", $"autoConnect=false; synchronizeMetadata=false{(string.IsNullOrWhiteSpace(m_loggingPath) ? "" : "; loggingPath=" + m_loggingPath)}", dataGapSettings.JoinKeyValuePairs(), connectionSettings.JoinKeyValuePairs()), - FilterExpression = this.OutputMeasurementKeys().Select(key => key.SignalID.ToString()).ToDelimitedString(';') - }; - - m_dataGapRecoverer.RecoveredMeasurements += DataGapRecoverer_RecoveredMeasurements; - m_dataGapRecoverer.StatusMessage += DataGapRecoverer_StatusMessage; - m_dataGapRecoverer.ProcessException += DataGapRecoverer_ProcessException; - m_dataGapRecoverer.Initialize(); - } - else - { - m_dataGapRecoveryEnabled = false; - } - } - } - } - else - { - m_dataGapRecoveryEnabled = false; - } - - if (settings.TryGetValue(nameof(BypassStatistics), out setting) && setting.ParseBoolean()) - { - BypassStatistics = true; - } - else - { - void statisticsCalculated(object sender, EventArgs args) - { - ResetMeasurementsPerSecondCounters(); - } - - StatisticsEngine.Register(this, "Subscriber", "SUB"); - StatisticsEngine.Calculated += statisticsCalculated; - Disposed += (_, _) => StatisticsEngine.Calculated -= statisticsCalculated; - } - - if (PersistConnectionForMetadata) - { - m_clientCommandChannel?.ConnectAsync(); - m_serverCommandChannel?.Start(); - } - - Initialized = true; - } - - // Initialize (or reinitialize) the output measurements associated with the data subscriber. - // Returns true if output measurements were updated, otherwise false if they remain the same. - private bool UpdateOutputMeasurements(bool initialCall = false) - { - IMeasurement[] originalOutputMeasurements = OutputMeasurements; - - // Reapply output measurements if reinitializing - this way filter expressions and/or sourceIDs - // will be reapplied. This can be important after a meta-data refresh which may have added new - // measurements that could now be applicable as desired output measurements. - if (!initialCall) - { - if (Settings.TryGetValue("outputMeasurements", out string setting)) - OutputMeasurements = ParseOutputMeasurements(DataSource, true, setting); - - OutputSourceIDs = OutputSourceIDs; - } - - // If active measurements are defined, attempt to defined desired subscription points from there - if ((SecurityMode == SecurityMode.TLS || FilterOutputMeasurements) && DataSource is not null && DataSource.Tables.Contains("ActiveMeasurements")) - { - try - { - // Filter to points associated with this subscriber that have been requested for subscription, are enabled and not owned locally - DataRow[] filteredRows = DataSource.Tables["ActiveMeasurements"].Select("Subscribed <> 0"); - List subscribedMeasurements = []; - - foreach (DataRow row in filteredRows) - { - // Create a new measurement for the provided field level information - Measurement measurement = new(); - - // Parse primary measurement identifier - Guid signalID = row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(); - - // Set measurement key if defined - MeasurementKey key = MeasurementKey.LookUpOrCreate(signalID, row["ID"].ToString()); - measurement.Metadata = key.Metadata; - subscribedMeasurements.Add(measurement); - } - - // Combine subscribed output measurement with any existing output measurement and return unique set - if (subscribedMeasurements.Count > 0) - OutputMeasurements = OutputMeasurements is null ? - subscribedMeasurements.ToArray() : - subscribedMeasurements.Concat(OutputMeasurements).Distinct().ToArray(); - } - catch (Exception ex) - { - // Errors here may not be catastrophic, this simply limits the auto-assignment of input measurement keys desired for subscription - OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to apply subscribed measurements to subscription filter: {ex.Message}", ex)); - } - } - - // Ensure that we are not attempting to subscribe to - // measurements that we know cannot be published - TryFilterOutputMeasurements(); - - // Determine if output measurements have changed - return originalOutputMeasurements.CompareTo(OutputMeasurements, false) != 0; - } - - // When synchronizing meta-data, the publisher sends meta-data for all possible signals we can subscribe to. - // Here we check each signal defined in OutputMeasurements to determine whether that signal was defined in - // the published meta-data rather than blindly attempting to subscribe to all signals. - private void TryFilterOutputMeasurements() - { - if (!FilterOutputMeasurements) - return; - - try - { - if (OutputMeasurements is null || DataSource is null || !DataSource.Tables.Contains("ActiveMeasurements")) - return; - - Guid signalID = Guid.Empty; - - // Have to use a Convert expression for DeviceID column in Select function - // here since SQLite doesn't report data types for COALESCE based columns - IEnumerable measurementIDs = DataSource.Tables["ActiveMeasurements"] - .Select($"Convert(DeviceID, 'System.String') = '{ID}'") - .Where(row => Guid.TryParse(row["SignalID"].ToNonNullString(), out signalID)) - .Select(_ => signalID); - - HashSet measurementIDSet = [..measurementIDs]; - - OutputMeasurements = OutputMeasurements.Where(measurement => measurementIDSet.Contains(measurement.ID)).ToArray(); - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Error when filtering output measurements by device ID: {ex.Message}", ex)); - } - } - - /// - /// Subscribes (or re-subscribes) to a data publisher for a set of data points. - /// - /// Configuration object that defines the subscription. - /// true if subscribe transmission was successful; otherwise false. - public bool Subscribe(SubscriptionInfo info) - { - StringBuilder connectionString = new(); - AssemblyInfo assemblyInfo = new(typeof(DataSubscriber).Assembly); - - connectionString.Append($"throttled={info.Throttled};"); - connectionString.Append($"publishInterval={info.PublishInterval};"); - connectionString.Append($"includeTime={info.IncludeTime};"); - connectionString.Append($"lagTime={info.LagTime};"); - connectionString.Append($"leadTime={info.LeadTime};"); - connectionString.Append($"useLocalClockAsRealTime={info.UseLocalClockAsRealTime};"); - connectionString.Append($"processingInterval={info.ProcessingInterval};"); - connectionString.Append($"useMillisecondResolution={info.UseMillisecondResolution};"); - connectionString.Append($"requestNaNValueFilter={info.RequestNaNValueFilter};"); - connectionString.Append($"assemblyInfo={{source=STTP GSF Library ({assemblyInfo.Name}.dll); version={assemblyInfo.Version.Major}.{assemblyInfo.Version.Minor}.{assemblyInfo.Version.Build}; updatedOn={assemblyInfo.BuildDate:yyyy-MM-dd HH:mm:ss} }};"); - - if (!string.IsNullOrWhiteSpace(info.FilterExpression)) - connectionString.Append($"filterExpression={{{info.FilterExpression}}};"); - - if (info.UdpDataChannel) - connectionString.Append($"dataChannel={{localport={info.DataChannelLocalPort}}};"); - - if (!string.IsNullOrWhiteSpace(info.StartTime)) - connectionString.Append($"startTimeConstraint={info.StartTime};"); - - if (!string.IsNullOrWhiteSpace(info.StopTime)) - connectionString.Append($"stopTimeConstraint={info.StopTime};"); - - if (!string.IsNullOrWhiteSpace(info.ConstraintParameters)) - connectionString.Append($"timeConstraintParameters={info.ConstraintParameters};"); - - if (!string.IsNullOrWhiteSpace(info.ExtraConnectionStringParameters)) - connectionString.Append($"{info.ExtraConnectionStringParameters};"); - - // Make sure not to monitor for data loss any faster than down-sample time on throttled connections - additionally - // you will want to make sure data stream monitor is twice lag-time to allow time for initial points to arrive. - if (info.Throttled && m_dataStreamMonitor is not null && m_dataStreamMonitor.Interval / 1000.0D < info.LagTime) - m_dataStreamMonitor.Interval = (int)(2.0D * info.LagTime * 1000.0D); - - // Set millisecond resolution member variable for compact measurement parsing - #pragma warning disable 618 - UseMillisecondResolution = info.UseMillisecondResolution; - #pragma warning restore 618 - - return Subscribe(info.UseCompactMeasurementFormat, connectionString.ToString()); - } - - /// - /// Subscribes (or re-subscribes) to a data publisher for an unsynchronized set of data points. - /// - /// Boolean value that determines if the compact measurement format should be used. Set to false for full fidelity measurement serialization; otherwise set to true for bandwidth conservation. - /// Boolean value that determines if data should be throttled at a set transmission interval or sent on change. - /// Filtering expression that defines the measurements that are being subscribed. - /// Desired UDP return data channel connection string to use for data packet transmission. Set to null to use TCP channel for data transmission. - /// Boolean value that determines if time is a necessary component in streaming data. - /// When is true, defines the data transmission speed in seconds (can be sub-second). - /// When is true, defines the allowed time deviation tolerance to real-time in seconds (can be sub-second). - /// When is true, defines boolean value that determines whether to use the local clock time as real-time. Set to false to use latest received measurement timestamp as real-time. - /// Defines a relative or exact start time for the temporal constraint to use for historical playback. - /// Defines a relative or exact stop time for the temporal constraint to use for historical playback. - /// Defines any temporal parameters related to the constraint to use for historical playback. - /// Defines the desired processing interval milliseconds, i.e., historical play back speed, to use when temporal constraints are defined. - /// Defines the interval, in seconds, at which data should be published when using a throttled subscription. - /// true if subscribe transmission was successful; otherwise false. - /// - /// - /// When the or temporal processing constraints are defined (i.e., not null), this - /// specifies the start and stop time over which the subscriber session will process data. Passing in null for the - /// and specifies the subscriber session will process data in standard, i.e., real-time, operation. - /// - /// - /// Except for the values of -1 and 0, the value specifies the desired historical playback data - /// processing interval in milliseconds. This is basically a delay, or timer interval, over which to process data. Setting this value - /// to -1 means to use the default processing interval while setting the value to 0 means to process data as fast as possible. - /// - /// - /// The and parameters can be specified in one of the - /// following formats: - /// - /// - /// Time Format - /// Format Description - /// - /// - /// 12-30-2000 23:59:59.033 - /// Absolute date and time. - /// - /// - /// * - /// Evaluates to . - /// - /// - /// *-20s - /// Evaluates to 20 seconds before . - /// - /// - /// *-10m - /// Evaluates to 10 minutes before . - /// - /// - /// *-1h - /// Evaluates to 1 hour before . - /// - /// - /// *-1d - /// Evaluates to 1 day before . - /// - /// - /// - /// - [Obsolete("Preferred method uses SubscriptionInfo object to subscribe.", false)] - public virtual bool Subscribe(bool compactFormat, bool throttled, string filterExpression, string dataChannel = null, bool includeTime = true, double lagTime = 10.0D, double leadTime = 5.0D, bool useLocalClockAsRealTime = false, string startTime = null, string stopTime = null, string constraintParameters = null, int processingInterval = -1, double publishInterval = -1.0D) - { - int port = 0; - - if (!string.IsNullOrWhiteSpace(dataChannel)) - { - Dictionary settings = dataChannel.ParseKeyValuePairs(); - - if (settings.TryGetValue("port", out string setting) || settings.TryGetValue("localPort", out setting)) - int.TryParse(setting, out port); - } - - return Subscribe(new SubscriptionInfo - { - UseCompactMeasurementFormat = compactFormat, - Throttled = throttled, - PublishInterval = publishInterval, - UdpDataChannel = port > 0, - DataChannelLocalPort = port, - FilterExpression = filterExpression, - IncludeTime = includeTime, - LagTime = lagTime, - LeadTime = leadTime, - UseLocalClockAsRealTime = useLocalClockAsRealTime, - StartTime = startTime, - StopTime = stopTime, - ConstraintParameters = constraintParameters, - ProcessingInterval = processingInterval, - UseMillisecondResolution = UseMillisecondResolution // When used as adapter, use configured option - }); - } - - /// - /// Subscribes (or re-subscribes) to a data publisher for a set of data points. - /// - /// Boolean value that determines if the compact measurement format should be used. Set to false for full fidelity measurement serialization; otherwise set to true for bandwidth conservation. - /// Connection string that defines required and optional parameters for the subscription. - /// true if subscribe transmission was successful; otherwise false. - public virtual bool Subscribe(bool compactFormat, string connectionString) - { - bool success = false; - - if (!string.IsNullOrWhiteSpace(connectionString)) - { - try - { - // Parse connection string to see if it contains a data channel definition - Dictionary settings = connectionString.ParseKeyValuePairs(); - UdpClient dataChannel = null; - - // Track specified time inclusion for later deserialization - m_includeTime = !settings.TryGetValue("includeTime", out string setting) || setting.ParseBoolean(); - - settings.TryGetValue("dataChannel", out setting); - - if (!string.IsNullOrWhiteSpace(setting)) - { - if ((CompressionModes & CompressionModes.TSSC) > 0) - { - // TSSC is a stateful compression algorithm which will not reliably support UDP - OnStatusMessage(MessageLevel.Warning, "Cannot use TSSC compression mode with UDP - special compression mode disabled"); - - // Disable TSSC compression processing - CompressionModes &= ~CompressionModes.TSSC; - } - - dataChannel = new UdpClient(setting) - { - ReceiveBufferSize = ushort.MaxValue, - MaxConnectionAttempts = -1 - }; - - dataChannel.ConnectAsync(); - } - - // Assign data channel client reference and attach to needed events - DataChannel = dataChannel; - - // Setup subscription packet - using BlockAllocatedMemoryStream buffer = new(); - DataPacketFlags flags = DataPacketFlags.NoFlags; - - if (compactFormat) - flags |= DataPacketFlags.Compact; - - // Write data packet flags into buffer - buffer.WriteByte((byte)flags); - - // Get encoded bytes of connection string - byte[] bytes = Encoding.GetBytes(connectionString); - - // Write encoded connection string length into buffer - buffer.Write(BigEndian.GetBytes(bytes.Length), 0, 4); - - // Encode connection string into buffer - buffer.Write(bytes, 0, bytes.Length); - - // Send subscribe server command with associated command buffer - success = SendServerCommand(ServerCommand.Subscribe, buffer.ToArray()); - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Error, new InvalidOperationException($"Exception occurred while trying to make publisher subscription: {ex.Message}", ex)); - } - } - else - { - OnProcessException(MessageLevel.Error, new InvalidOperationException("Cannot make publisher subscription without a connection string.")); - } - - return success; - } - - /// - /// Subscribes to a data publisher based on currently configured adapter settings. - /// - /// true if subscribe command was sent successfully; otherwise false. - [AdapterCommand("Subscribes to data publisher.", "Administrator", "Editor")] - [EditorBrowsable(EditorBrowsableState.Advanced)] // Method exists for remote console execution - public virtual bool Subscribe() - { - return SubscribeToOutputMeasurements(!m_metadataRefreshPending); - } - - /// - /// Unsubscribes from a data publisher. - /// - /// true if unsubscribe command was sent successfully; otherwise false. - [AdapterCommand("Unsubscribes from data publisher.", "Administrator", "Editor")] - public virtual bool Unsubscribe() - { - return SendServerCommand(ServerCommand.Unsubscribe); - } - - /// - /// Returns the measurements signal IDs that were authorized after the last successful subscription request. - /// - [AdapterCommand("Gets authorized signal IDs from last subscription request.", "Administrator", "Editor", "Viewer")] - public virtual Guid[] GetAuthorizedSignalIDs() - { - lock (m_signalIndexCacheLock) - return m_signalIndexCache?[m_cacheIndex] is null ? [] : m_signalIndexCache[m_cacheIndex].AuthorizedSignalIDs; - } - - /// - /// Returns the measurements signal IDs that were unauthorized after the last successful subscription request. - /// - [AdapterCommand("Gets unauthorized signal IDs from last subscription request.", "Administrator", "Editor", "Viewer")] - public virtual Guid[] GetUnauthorizedSignalIDs() - { - lock (m_signalIndexCacheLock) - return m_signalIndexCache?[m_cacheIndex] is null ? [] : m_signalIndexCache[m_cacheIndex].UnauthorizedSignalIDs; - } - - /// - /// Resets the counters for the lifetime statistics without interrupting the adapter's operations. - /// - [AdapterCommand("Resets the counters for the lifetime statistics without interrupting the adapter's operations.", "Administrator", "Editor")] - public virtual void ResetLifetimeCounters() - { - LifetimeMeasurements = 0L; - TotalBytesReceived = 0L; - m_lifetimeTotalLatency = 0L; - m_lifetimeMinimumLatency = 0L; - m_lifetimeMaximumLatency = 0L; - m_lifetimeLatencyMeasurements = 0L; - } - - /// - /// Initiate a meta-data refresh. - /// - [AdapterCommand("Initiates a meta-data refresh.", "Administrator", "Editor")] - public virtual void RefreshMetadata() - { - SendServerCommand(ServerCommand.MetaDataRefresh, MetadataFilters); - } - - /// - /// Log a data gap for data gap recovery. - /// - /// The string representing the data gap. - [AdapterCommand("Logs a data gap for data gap recovery.", "Administrator", "Editor")] - public virtual void LogDataGap(string timeString) - { - DateTimeOffset end = default; - string[] split = timeString.Split(';'); - - if (!m_dataGapRecoveryEnabled) - throw new InvalidOperationException("Data gap recovery is not enabled."); - - if (split.Length != 2) - throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733"); - - string startTime = split[0]; - string endTime = split[1]; - - bool parserSuccessful = - DateTimeOffset.TryParse(startTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out DateTimeOffset start) && - DateTimeOffset.TryParse(endTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out end); - - if (!parserSuccessful) - throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733"); - - m_dataGapRecoverer?.LogDataGap(start, end, true); - } - - /// - /// Remove a data gap from data gap recovery. - /// - /// The string representing the data gap. - [AdapterCommand("Removes a data gap from data gap recovery.", "Administrator", "Editor")] - public virtual string RemoveDataGap(string timeString) - { - DateTimeOffset end = default; - string[] split = timeString.Split(';'); - - if (!m_dataGapRecoveryEnabled) - throw new InvalidOperationException("Data gap recovery is not enabled."); - - if (split.Length != 2) - throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733"); - - string startTime = split[0]; - string endTime = split[1]; - - bool parserSuccessful = - DateTimeOffset.TryParse(startTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out DateTimeOffset start) && - DateTimeOffset.TryParse(endTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out end); - - if (!parserSuccessful) - throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733"); - - if (m_dataGapRecoverer?.RemoveDataGap(start, end) ?? false) - return "Data gap successfully removed."; - - return "Data gap not found."; - } - - /// - /// Displays the contents of the outage log. - /// - /// The contents of the outage log. - [AdapterCommand("Displays data gaps queued for data gap recovery.", "Administrator", "Editor", "Viewer")] - public virtual string DumpOutageLog() - { - if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null) - return Environment.NewLine + m_dataGapRecoverer.DumpOutageLog(); - - throw new InvalidOperationException("Data gap recovery not enabled"); - } - - /// - /// Gets the status of the temporal used by the data gap recovery module. - /// - /// Status of the temporal used by the data gap recovery module. - [AdapterCommand("Gets the status of the temporal subscription used by the data gap recovery module.", "Administrator", "Editor", "Viewer")] - public virtual string GetDataGapRecoverySubscriptionStatus() - { - if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null) - return m_dataGapRecoverer.TemporalSubscriptionStatus; - - return "Data gap recovery not enabled"; - } - - /// - /// Spawn meta-data synchronization. - /// - /// to use for synchronization. - /// - /// This method makes sure only one meta-data synchronization happens at a time. - /// - public void SynchronizeMetadata(DataSet metadata) - { - try - { - m_receivedMetadata = metadata; - m_synchronizeMetadataOperation.RunOnceAsync(); - } - catch (Exception ex) - { - // Process exception for logging - OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to queue meta-data synchronization: {ex.Message}", ex)); - } - } - - /// - /// Sends a server command to the publisher connection with associated data. - /// - /// to send. - /// String based command data to send to server. - /// true if transmission was successful; otherwise false. - public virtual bool SendServerCommand(ServerCommand commandCode, string message) - { - if (!string.IsNullOrWhiteSpace(message)) - { - using BlockAllocatedMemoryStream buffer = new(); - byte[] bytes = Encoding.GetBytes(message); - - buffer.Write(BigEndian.GetBytes(bytes.Length), 0, 4); - buffer.Write(bytes, 0, bytes.Length); - - return SendServerCommand(commandCode, buffer.ToArray()); - } - - return SendServerCommand(commandCode); - } - - /// - /// Sends a server command to the publisher connection. - /// - /// to send. - /// Optional command data to send. - /// true if transmission was successful; otherwise false. - public virtual bool SendServerCommand(ServerCommand commandCode, byte[] data = null) - { - if (m_clientCommandChannel?.CurrentState == ClientState.Connected || m_serverCommandChannel?.CurrentState == ServerState.Running && m_activeClientID != Guid.Empty) - { - try - { - using BlockAllocatedMemoryStream commandPacket = new(); - // Write command code into command packet - commandPacket.WriteByte((byte)commandCode); - - // Write command buffer into command packet - if (data is not null && data.Length > 0) - commandPacket.Write(data, 0, data.Length); - - // Send command packet to publisher - m_clientCommandChannel?.SendAsync(commandPacket.ToArray(), 0, (int)commandPacket.Length); - m_serverCommandChannel?.SendToAsync(m_activeClientID, commandPacket.ToArray(), 0, (int)commandPacket.Length); - m_metadataRefreshPending = commandCode == ServerCommand.MetaDataRefresh; - - return true; - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Error, new InvalidOperationException($"Exception occurred while trying to send server command \"{commandCode}\" to publisher: {ex.Message}", ex)); - } - } - else - OnProcessException(MessageLevel.Error, new InvalidOperationException($"Subscriber is currently unconnected. Cannot send server command \"{commandCode}\" to publisher.")); - - return false; - } - - /// - /// Attempts to connect to this . - /// - protected override void AttemptConnection() - { - if (!this.TemporalConstraintIsDefined() && !SupportsRealTimeProcessing) - return; - - long now = UseLocalClockAsRealTime ? DateTime.UtcNow.Ticks : 0L; - List> statisticsHelpers = m_statisticsHelpers; - - m_registerStatisticsOperation.RunOnceAsync(); - m_expectedBufferBlockSequenceNumber = 0u; - m_commandChannelConnectionAttempts = 0; - m_dataChannelConnectionAttempts = 0; - - m_subscribed = false; - m_keyIVs = null; - TotalBytesReceived = 0L; - m_monitoredBytesReceived = 0L; - m_lastBytesReceived = 0; - m_lastReceivedAt = DateTime.MinValue; - - if (!PersistConnectionForMetadata) - { - m_clientCommandChannel?.ConnectAsync(); - m_serverCommandChannel?.Start(); - } - else - { - OnConnected(); - } - - if (PersistConnectionForMetadata && CommandChannelConnected) - SubscribeToOutputMeasurements(true); - - if (UseLocalClockAsRealTime && m_subscribedDevicesTimer is null) - { - m_subscribedDevicesTimer = Common.TimerScheduler.CreateTimer(1000); - m_subscribedDevicesTimer.Elapsed += SubscribedDevicesTimer_Elapsed; - } - - if (statisticsHelpers is not null) - { - m_realTime = 0L; - m_lastStatisticsHelperUpdate = 0L; - - foreach (DeviceStatisticsHelper statisticsHelper in statisticsHelpers) - statisticsHelper.Reset(now); - } - - if (UseLocalClockAsRealTime) - m_subscribedDevicesTimer.Start(); - } - - /// - /// Attempts to disconnect from this . - /// - protected override void AttemptDisconnection() - { - // Unregister device statistics - m_registerStatisticsOperation.RunOnceAsync(); - - // Stop data stream monitor - if (m_dataStreamMonitor is not null) - m_dataStreamMonitor.Enabled = false; - - // Disconnect command channel - if (!PersistConnectionForMetadata) - { - m_clientCommandChannel?.Disconnect(); - m_serverCommandChannel?.Stop(); - } - - m_activeClientID = Guid.Empty; - m_subscribedDevicesTimer?.Stop(); - m_metadataRefreshPending = false; - } - - /// - /// Gets a short one-line status of this . - /// - /// Maximum length of the status message. - /// Text of the status message. - public override string GetShortStatus(int maxLength) - { - if (m_clientCommandChannel?.CurrentState == ClientState.Connected) - return $"Subscriber connection has received {ProcessedMeasurements:N0} data points".CenterText(maxLength); - - if (m_serverCommandChannel?.CurrentState == ServerState.Running && m_activeClientID != Guid.Empty) - return $"Subscriber server-based connection has received {ProcessedMeasurements:N0} points".CenterText(maxLength); - - return "Subscriber is not connected.".CenterText(maxLength); - } - - /// - /// Get message from string based response. - /// - /// Response buffer. - /// Start index of response message. - /// Length of response message. - /// Decoded response string. - protected string InterpretResponseMessage(byte[] buffer, int startIndex, int length) - { - return Encoding.GetString(buffer, startIndex, length); - } - - // Restarts the subscriber. - private void Restart() - { - try - { - base.Start(); - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Warning, ex); - } - } - - private void ProcessServerResponse(byte[] buffer, int length) - { - // Currently this work is done on the async socket completion thread, make sure work to be done is timely and if the response processing - // is coming in via the command channel and needs to send a command back to the server, it should be done on a separate thread... - if (buffer is null || length <= 0) - return; - - int startIndex = 0; - - while (startIndex < length) - { - try - { - Dictionary> subscribedDevicesLookup; - DeviceStatisticsHelper statisticsHelper; - - ServerResponse responseCode = (ServerResponse)buffer[startIndex]; - ServerCommand commandCode = (ServerCommand)buffer[startIndex + 1]; - int responseLength = BigEndian.ToInt32(buffer, startIndex + 2); - int responseIndex = startIndex + DataPublisher.ClientResponseHeaderSize; - byte[][][] keyIVs; - - startIndex = responseIndex + responseLength; - - // Disconnect any established UDP data channel upon successful unsubscribe - if (commandCode == ServerCommand.Unsubscribe && responseCode == ServerResponse.Succeeded) - DataChannel = null; - - if (!IsUserCommand(commandCode)) - OnReceivedServerResponse(responseCode, commandCode); - else - OnReceivedUserCommandResponse(commandCode, responseCode, buffer, responseIndex, responseLength); - - switch (responseCode) - { - case ServerResponse.Succeeded: - switch (commandCode) - { - case ServerCommand.Subscribe: - OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}"); - m_subscribed = true; - break; - case ServerCommand.Unsubscribe: - OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}"); - m_subscribed = false; - if (m_dataStreamMonitor is not null) - m_dataStreamMonitor.Enabled = false; - break; - case ServerCommand.RotateCipherKeys: - OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}"); - break; - case ServerCommand.MetaDataRefresh: - OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": latest meta-data received."); - OnMetaDataReceived(DeserializeMetadata(buffer.BlockCopy(responseIndex, responseLength))); - m_metadataRefreshPending = false; - break; - } - break; - case ServerResponse.Failed: - OnStatusMessage(MessageLevel.Info, $"Failure code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}"); - - if (commandCode == ServerCommand.MetaDataRefresh) - m_metadataRefreshPending = false; - break; - case ServerResponse.DataPacket: - { - long now = DateTime.UtcNow.Ticks; - - // Deserialize data packet - List measurements = []; - Ticks timestamp = default; - - if (TotalBytesReceived == 0) - { - // At the point when data is being received, data monitor should be enabled - if (!(m_dataStreamMonitor?.Enabled ?? false)) - m_dataStreamMonitor.Enabled = true; - - // Establish run-time log for subscriber - if (AutoConnect || m_dataGapRecoveryEnabled) - { - if (m_runTimeLog is null) - { - m_runTimeLog = new RunTimeLog { FileName = GetLoggingPath($"{Name}_RunTimeLog.txt") }; - m_runTimeLog.ProcessException += RunTimeLog_ProcessException; - m_runTimeLog.Initialize(); - } - else - { - // Mark the start of any data transmissions - m_runTimeLog.StartTime = DateTimeOffset.UtcNow; - m_runTimeLog.Enabled = true; - } - } - - // The duration between last disconnection and start of data transmissions - // represents a gap in data - if data gap recovery is enabled, we log - // this as a gap for recovery: - if (m_dataGapRecoveryEnabled) - m_dataGapRecoverer?.LogDataGap(m_runTimeLog.StopTime, DateTimeOffset.UtcNow); - } - - // Track total data packet bytes received from any channel - TotalBytesReceived += m_lastBytesReceived; - m_monitoredBytesReceived += m_lastBytesReceived; - - // Get data packet flags - DataPacketFlags flags = (DataPacketFlags)buffer[responseIndex]; - responseIndex++; - - SignalIndexCache signalIndexCache; - bool compactMeasurementFormat = (byte)(flags & DataPacketFlags.Compact) > 0; - bool compressedPayload = (byte)(flags & DataPacketFlags.Compressed) > 0; - int cipherIndex = (flags & DataPacketFlags.CipherIndex) > 0 ? 1 : 0; - int cacheIndex = (flags & DataPacketFlags.CacheIndex) > 0 ? 1 : 0; - byte[] packet = buffer; - int packetLength = responseLength - 1; - - lock (m_signalIndexCacheLock) - signalIndexCache = m_signalIndexCache?[cacheIndex]; - - // Decrypt data packet payload if keys are available - if (m_keyIVs is not null) - { - // Get a local copy of volatile keyIVs reference since this can change at any time - keyIVs = m_keyIVs; - - // Decrypt payload portion of data packet - packet = Common.SymmetricAlgorithm.Decrypt(packet, responseIndex, packetLength, keyIVs[cipherIndex][0], keyIVs[cipherIndex][1]); - responseIndex = 0; - packetLength = packet.Length; - } - - // Deserialize number of measurements that follow - int count = BigEndian.ToInt32(packet, responseIndex); - responseIndex += 4; - packetLength -= 4; - - if (compressedPayload) - { - if (CompressionModes.HasFlag(CompressionModes.TSSC)) - { - if (signalIndexCache is not null) - { - try - { - // Decompress TSSC serialized measurements from payload - ParseTSSCMeasurements(packet, packetLength, signalIndexCache, ref responseIndex, measurements); - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Error, new InvalidOperationException($"Decompression failure: (Decoded {measurements.Count} of {count} measurements) - {ex.Message}", ex)); - } - } - } - else - { - OnProcessException(MessageLevel.Error, new InvalidOperationException("Decompression failure: Unexpected compression type in use - STTP currently only supports TSSC payload compression")); - } - } - else - { - // Deserialize measurements - for (int i = 0; i < count; i++) - { - if (!compactMeasurementFormat) - { - // Deserialize full measurement format - SerializableMeasurement measurement = new(Encoding); - responseIndex += measurement.ParseBinaryImage(packet, responseIndex, length - responseIndex); - measurements.Add(measurement); - } - else if (signalIndexCache is not null) - { - #pragma warning disable 618 - bool useMillisecondResolution = UseMillisecondResolution; - #pragma warning restore 618 - - // Deserialize compact measurement format - CompactMeasurement measurement = new(signalIndexCache, m_includeTime, m_baseTimeOffsets, m_timeIndex, useMillisecondResolution); - responseIndex += measurement.ParseBinaryImage(packet, responseIndex, length - responseIndex); - - // Apply timestamp from frame if not included in transmission - if (!measurement.IncludeTime) - measurement.Timestamp = timestamp; - - measurements.Add(measurement); - } - else if (m_lastMissingCacheWarning + MissingCacheWarningInterval < now) - { - // Warning message for missing signal index cache - if (m_lastMissingCacheWarning != 0L) - OnStatusMessage(MessageLevel.Error, "Signal index cache has not arrived. No compact measurements can be parsed."); - - m_lastMissingCacheWarning = now; - } - } - } - - // Calculate statistics - subscribedDevicesLookup = m_subscribedDevicesLookup; - statisticsHelper = null; - - if (subscribedDevicesLookup is not null) - { - IEnumerable, IMeasurement>> deviceGroups = measurements - .Where(measurement => subscribedDevicesLookup.TryGetValue(measurement.ID, out statisticsHelper)) - .Select(measurement => Tuple.Create(statisticsHelper, measurement)) - .ToList() - .GroupBy(tuple => tuple.Item1, tuple => tuple.Item2); - - foreach (IGrouping, IMeasurement> deviceGroup in deviceGroups) - { - statisticsHelper = deviceGroup.Key; - - foreach (IGrouping frame in deviceGroup.GroupBy(measurement => measurement.Timestamp)) - { - // Determine the number of measurements received with valid values - const MeasurementStateFlags ErrorFlags = MeasurementStateFlags.BadData | MeasurementStateFlags.BadTime | MeasurementStateFlags.SystemError; - - static bool hasError(MeasurementStateFlags stateFlags) - { - return (stateFlags & ErrorFlags) != MeasurementStateFlags.Normal; - } - - int measurementsReceived = frame.Count(measurement => !double.IsNaN(measurement.Value)); - int measurementsWithError = frame.Count(measurement => !double.IsNaN(measurement.Value) && hasError(measurement.StateFlags)); - - IMeasurement statusFlags = null; - IMeasurement frequency = null; - IMeasurement deltaFrequency = null; - - // Attempt to update real-time - if (!UseLocalClockAsRealTime && frame.Key > m_realTime) - m_realTime = frame.Key; - - // Search the frame for status flags, frequency, and delta frequency - foreach (IMeasurement measurement in frame) - { - if (measurement.ID == statisticsHelper.Device.StatusFlagsID) - statusFlags = measurement; - else if (measurement.ID == statisticsHelper.Device.FrequencyID) - frequency = measurement; - else if (measurement.ID == statisticsHelper.Device.DeltaFrequencyID) - deltaFrequency = measurement; - } - - // If we are receiving status flags for this device, - // count the data quality, time quality, and device errors - if (statusFlags is not null) - { - uint commonStatusFlags = (uint)statusFlags.Value; - - if ((commonStatusFlags & (uint)Bits.Bit19) > 0) - statisticsHelper.Device.DataQualityErrors++; - - if ((commonStatusFlags & (uint)Bits.Bit18) > 0) - statisticsHelper.Device.TimeQualityErrors++; - - if ((commonStatusFlags & (uint)Bits.Bit16) > 0) - statisticsHelper.Device.DeviceErrors++; - - measurementsReceived--; - - if (hasError(statusFlags.StateFlags)) - measurementsWithError--; - } - - // Zero is not a valid value for frequency. - // If frequency is zero, invalidate both frequency and delta frequency - if (frequency is not null) - { - if (!this.TemporalConstraintIsDefined()) - statisticsHelper.MarkDeviceTimestamp(frequency.Timestamp); - - if (frequency.Value == 0.0D) - { - if (deltaFrequency is null || double.IsNaN(deltaFrequency.Value)) - measurementsReceived--; - else - measurementsReceived -= 2; - - if (hasError(frequency.StateFlags)) - { - if (deltaFrequency is null || double.IsNaN(deltaFrequency.Value)) - measurementsWithError--; - else - measurementsWithError -= 2; - } - } - } - - // Track the number of measurements received - statisticsHelper.AddToMeasurementsReceived(measurementsReceived); - statisticsHelper.AddToMeasurementsWithError(measurementsWithError); - } - } - } - - OnNewMeasurements(measurements); - - // Gather statistics on received data - DateTime timeReceived = RealTime; - - if (!UseLocalClockAsRealTime && timeReceived.Ticks - m_lastStatisticsHelperUpdate > Ticks.PerSecond) - { - UpdateStatisticsHelpers(); - m_lastStatisticsHelperUpdate = m_realTime; - } - - LifetimeMeasurements += measurements.Count; - UpdateMeasurementsPerSecond(timeReceived, measurements.Count); - - foreach (IMeasurement measurement in measurements) - { - long latency = timeReceived.Ticks - (long)measurement.Timestamp; - - // Throw out latencies that exceed one hour as invalid - if (Math.Abs(latency) > Time.SecondsPerHour * Ticks.PerSecond) - continue; - - if (m_lifetimeMinimumLatency > latency || m_lifetimeMinimumLatency == 0) - m_lifetimeMinimumLatency = latency; - - if (m_lifetimeMaximumLatency < latency || m_lifetimeMaximumLatency == 0) - m_lifetimeMaximumLatency = latency; - - m_lifetimeTotalLatency += latency; - m_lifetimeLatencyMeasurements++; - } - break; - } - case ServerResponse.BufferBlock: - { - // Buffer block received - wrap as a buffer block measurement and expose back to consumer - uint sequenceNumber = BigEndian.ToUInt32(buffer, responseIndex); - int bufferCacheIndex = (int)(sequenceNumber - m_expectedBufferBlockSequenceNumber); - int signalCacheIndex = Version > 1 ? buffer[responseIndex + 4] : 0; - - // Check if this buffer block has already been processed (e.g., mistaken retransmission due to timeout) - if (bufferCacheIndex >= 0 && (bufferCacheIndex >= m_bufferBlockCache.Count || m_bufferBlockCache[bufferCacheIndex] is null)) - { - // Send confirmation that buffer block is received - SendServerCommand(ServerCommand.ConfirmBufferBlock, buffer.BlockCopy(responseIndex, 4)); - - if (Version > 1) - responseIndex += 1; - - // Get measurement key from signal index cache - int signalIndex = BigEndian.ToInt32(buffer, responseIndex + 4); - - SignalIndexCache signalIndexCache; - - lock (m_signalIndexCacheLock) - signalIndexCache = m_signalIndexCache?[signalCacheIndex]; - - if (signalIndexCache is null || !signalIndexCache.Reference.TryGetValue(signalIndex, out MeasurementKey measurementKey)) - throw new InvalidOperationException($"Failed to find associated signal identification for runtime ID {signalIndex}"); - - // Skip the sequence number and signal index when creating the buffer block measurement - BufferBlockMeasurement bufferBlockMeasurement = new(buffer, responseIndex + 8, responseLength - 8) - { - Metadata = measurementKey.Metadata - }; - - // Determine if this is the next buffer block in the sequence - if (sequenceNumber == m_expectedBufferBlockSequenceNumber) - { - List bufferBlockMeasurements = []; - int i; - - // Add the buffer block measurement to the list of measurements to be published - bufferBlockMeasurements.Add(bufferBlockMeasurement); - m_expectedBufferBlockSequenceNumber++; - - // Add cached buffer block measurements to the list of measurements to be published - for (i = 1; i < m_bufferBlockCache.Count; i++) - { - if (m_bufferBlockCache[i] is null) - break; - - bufferBlockMeasurements.Add(m_bufferBlockCache[i]); - m_expectedBufferBlockSequenceNumber++; - } - - // Remove published measurements from the buffer block queue - if (m_bufferBlockCache.Count > 0) - m_bufferBlockCache.RemoveRange(0, i); - - // Publish measurements - OnNewMeasurements(bufferBlockMeasurements); - } - else - { - // Ensure that the list has at least as many - // elements as it needs to cache this measurement - for (int i = m_bufferBlockCache.Count; i <= bufferCacheIndex; i++) - m_bufferBlockCache.Add(null); - - // Insert this buffer block into the proper location in the list - m_bufferBlockCache[bufferCacheIndex] = bufferBlockMeasurement; - } - } - - LifetimeMeasurements += 1; - UpdateMeasurementsPerSecond(DateTime.UtcNow, 1); - break; - } - case ServerResponse.DataStartTime: - // Raise data start time event - OnDataStartTime(BigEndian.ToInt64(buffer, responseIndex)); - break; - case ServerResponse.ProcessingComplete: - // Raise input processing completed event - OnProcessingComplete(InterpretResponseMessage(buffer, responseIndex, responseLength)); - break; - case ServerResponse.UpdateSignalIndexCache: - { - int version = Version; - int cacheIndex = 0; - - // Get active cache index - if (version > 1) - cacheIndex = buffer[responseIndex++]; - - // Deserialize new signal index cache - SignalIndexCache remoteSignalIndexCache = DeserializeSignalIndexCache(buffer.BlockCopy(responseIndex, responseLength)); - SignalIndexCache signalIndexCache = new(DataSource, remoteSignalIndexCache); - - lock (m_signalIndexCacheLock) - { - Interlocked.CompareExchange(ref m_signalIndexCache, new SignalIndexCache[version > 1 ? 2 : 1], null); - - m_signalIndexCache[cacheIndex] = signalIndexCache; - m_remoteSignalIndexCache = remoteSignalIndexCache; - m_cacheIndex = cacheIndex; - } - - if (version > 1) - SendServerCommand(ServerCommand.ConfirmSignalIndexCache); - - FixExpectedMeasurementCounts(); - break; - } - case ServerResponse.UpdateBaseTimes: - // Get active time index - m_timeIndex = BigEndian.ToInt32(buffer, responseIndex); - responseIndex += 4; - - // Deserialize new base time offsets - m_baseTimeOffsets = [BigEndian.ToInt64(buffer, responseIndex), BigEndian.ToInt64(buffer, responseIndex + 8)]; - break; - case ServerResponse.UpdateCipherKeys: - // Move past active cipher index (not currently used anywhere else) - responseIndex++; - - // Extract remaining response - byte[] bytes = buffer.BlockCopy(responseIndex, responseLength - 1); - - // Deserialize new cipher keys - keyIVs = new byte[2][][]; - keyIVs[EvenKey] = new byte[2][]; - keyIVs[OddKey] = new byte[2][]; - - int index = 0; - - // Read even key size - int bufferLen = BigEndian.ToInt32(bytes, index); - index = 4; - - // Read even key - keyIVs[EvenKey][KeyIndex] = new byte[bufferLen]; - Buffer.BlockCopy(bytes, index, keyIVs[EvenKey][KeyIndex], 0, bufferLen); - index += bufferLen; - - // Read even initialization vector size - bufferLen = BigEndian.ToInt32(bytes, index); - index += 4; - - // Read even initialization vector - keyIVs[EvenKey][IVIndex] = new byte[bufferLen]; - Buffer.BlockCopy(bytes, index, keyIVs[EvenKey][IVIndex], 0, bufferLen); - index += bufferLen; - - // Read odd key size - bufferLen = BigEndian.ToInt32(bytes, index); - index += 4; - - // Read odd key - keyIVs[OddKey][KeyIndex] = new byte[bufferLen]; - Buffer.BlockCopy(bytes, index, keyIVs[OddKey][KeyIndex], 0, bufferLen); - index += bufferLen; - - // Read odd initialization vector size - bufferLen = BigEndian.ToInt32(bytes, index); - index += 4; - - // Read odd initialization vector - keyIVs[OddKey][IVIndex] = new byte[bufferLen]; - Buffer.BlockCopy(bytes, index, keyIVs[OddKey][IVIndex], 0, bufferLen); - //index += bufferLen; - - // Exchange keys - m_keyIVs = keyIVs; - - OnStatusMessage(MessageLevel.Info, "Successfully established new cipher keys for data packet transmissions."); - break; - case ServerResponse.Notify: - // Skip the 4-byte hash - string message = Encoding.GetString(buffer, responseIndex + 4, responseLength - 4); - - // Display notification - OnStatusMessage(MessageLevel.Info, $"NOTIFICATION: {message}"); - OnNotificationReceived(message); - - // Send confirmation of receipt of the notification - SendServerCommand(ServerCommand.ConfirmNotification, buffer.BlockCopy(responseIndex, 4)); - break; - case ServerResponse.ConfigurationChanged: - OnStatusMessage(MessageLevel.Info, "Received notification from publisher that configuration has changed."); - OnServerConfigurationChanged(); - - // Initiate meta-data refresh when publisher configuration has changed - we only do this - // for automatic connections since API style connections have to manually initiate a - // meta-data refresh. API style connection should attach to server configuration changed - // event and request meta-data refresh to complete automated cycle. - if (AutoConnect && AutoSynchronizeMetadata) - SendServerCommand(ServerCommand.MetaDataRefresh, MetadataFilters); - break; - } - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to process publisher response packet due to exception: {ex.Message}", ex)); - } - } - } - - private void ParseTSSCMeasurements(byte[] buffer, int packetLength, SignalIndexCache signalIndexCache, ref int responseIndex, List measurements) - { - TsscDecoder decoder = signalIndexCache.TsscDecoder; - bool newDecoder = false; - - // Use TSSC compression to decompress measurements - if (decoder is null) - { - decoder = signalIndexCache.TsscDecoder = new TsscDecoder(); - decoder.SequenceNumber = 0; - newDecoder = true; - } - - if (buffer[responseIndex] != 85) - throw new Exception($"TSSC version not recognized: {buffer[responseIndex]}"); - - responseIndex++; - - int sequenceNumber = BigEndian.ToUInt16(buffer, responseIndex); - responseIndex += 2; - - if (sequenceNumber == 0) - { - if (!newDecoder) - { - if (decoder.SequenceNumber > 0) - OnStatusMessage(MessageLevel.Info, $"TSSC algorithm reset before sequence number: {decoder.SequenceNumber}", "TSSC"); - - decoder = signalIndexCache.TsscDecoder = new TsscDecoder(); - decoder.SequenceNumber = 0; - } - } - - if (decoder.SequenceNumber != sequenceNumber) - { - OnProcessException(MessageLevel.Warning, new InvalidDataException($"TSSC is out of sequence. Expecting: {decoder.SequenceNumber}, Received: {sequenceNumber} -- resetting connection.")); - Start(); - return; - } - - decoder.SetBuffer(buffer, responseIndex, packetLength - 3); - - while (decoder.TryGetMeasurement(out int id, out long time, out uint quality, out float value)) - { - if (!signalIndexCache.Reference.TryGetValue(id, out MeasurementKey key) || key is null) - continue; - - Measurement measurement = new() - { - Metadata = key.Metadata, - Timestamp = time, - StateFlags = (MeasurementStateFlags)quality, - Value = value - }; - - measurements.Add(measurement); - } - - decoder.SequenceNumber++; - - // Do not increment to 0 on roll-over - if (decoder.SequenceNumber == 0) - decoder.SequenceNumber = 1; - } - - private static bool IsUserCommand(ServerCommand command) - { - ServerCommand[] userCommands = - [ - ServerCommand.UserCommand00, - ServerCommand.UserCommand01, - ServerCommand.UserCommand02, - ServerCommand.UserCommand03, - ServerCommand.UserCommand04, - ServerCommand.UserCommand05, - ServerCommand.UserCommand06, - ServerCommand.UserCommand07, - ServerCommand.UserCommand08, - ServerCommand.UserCommand09, - ServerCommand.UserCommand10, - ServerCommand.UserCommand11, - ServerCommand.UserCommand12, - ServerCommand.UserCommand13, - ServerCommand.UserCommand14, - ServerCommand.UserCommand15 - ]; - - return userCommands.Contains(command); - } - - // Handles auto-connection subscription initialization - private void StartSubscription() - { - SubscribeToOutputMeasurements(!AutoSynchronizeMetadata); - - // Initiate meta-data refresh - if (AutoSynchronizeMetadata && !this.TemporalConstraintIsDefined()) - SendServerCommand(ServerCommand.MetaDataRefresh, MetadataFilters); - } - - private bool SubscribeToOutputMeasurements(bool metaDataRefreshCompleted) - { - StringBuilder filterExpression = new(); - string dataChannel = null; - string startTimeConstraint = null; - string stopTimeConstraint = null; - int processingInterval = -1; - - // If TCP command channel is defined separately, then base connection string defines data channel - if (Settings.ContainsKey("commandChannel")) - dataChannel = ConnectionString; - - if (this.TemporalConstraintIsDefined()) - { - startTimeConstraint = StartTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fffffff"); - stopTimeConstraint = StopTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fffffff"); - processingInterval = ProcessingInterval; - } - - MeasurementKey[] outputMeasurementKeys = AutoStart - ? this.OutputMeasurementKeys() - : RequestedOutputMeasurementKeys; - - if (outputMeasurementKeys is not null && outputMeasurementKeys.Length > 0) - { - // TODO: Handle "continued" subscribe operations so connection string size can be fixed - foreach (MeasurementKey measurementKey in outputMeasurementKeys) - { - if (filterExpression.Length > 0) - filterExpression.Append(';'); - - // Subscribe by associated Guid... - filterExpression.Append(measurementKey.SignalID); - } - - // Start unsynchronized subscription - #pragma warning disable 618 - return Subscribe(true, Throttled, filterExpression.ToString(), dataChannel, startTime: startTimeConstraint, stopTime: stopTimeConstraint, processingInterval: processingInterval, publishInterval: PublishInterval); - #pragma warning restore 618 - } - - Unsubscribe(); - - if (AutoStart && metaDataRefreshCompleted) - OnStatusMessage(MessageLevel.Error, "No measurements are currently defined for subscription."); - - return false; - } - - /// - /// Handles meta-data synchronization to local system. - /// - /// - /// This function should only be initiated from call to to make - /// sure only one meta-data synchronization happens at once. Users can override this method to customize - /// process of meta-data synchronization. - /// - protected virtual void SynchronizeMetadata() - { - bool dataMonitoringEnabled = false; - - // TODO: This function is complex and very closely tied to the current time-series data schema - perhaps it should be moved outside this class and referenced - // TODO: as a delegate that can be assigned and called to allow other schemas as well. DataPublisher is already very flexible in what data it can deliver. - try - { - DataSet metadata = m_receivedMetadata; - - // Only perform database synchronization if meta-data has changed since last update - if (!SynchronizedMetadataChanged(metadata)) - return; - - if (metadata is null) - { - OnStatusMessage(MessageLevel.Error, "Meta-data synchronization was not performed, deserialized dataset was empty."); - return; - } - - // Reset data stream monitor while meta-data synchronization is in progress - if (m_dataStreamMonitor?.Enabled ?? false) - { - m_dataStreamMonitor.Enabled = false; - dataMonitoringEnabled = true; - } - - // Track total meta-data synchronization process time - Ticks startTime = DateTime.UtcNow.Ticks; - DateTime latestUpdateTime = DateTime.MinValue; - - // Open the configuration database using settings found in the config file - using (AdoDataConnection database = new("systemSettings")) - using (IDbCommand command = database.Connection.CreateCommand()) - { - IDbTransaction transaction = null; - - if (UseTransactionForMetadata) - transaction = database.Connection.BeginTransaction(database.DefaultIsolationLevel); - - try - { - if (transaction is not null) - command.Transaction = transaction; - - // Query the actual record ID based on the known run-time ID for this subscriber device - object sourceID = command.ExecuteScalar($"SELECT SourceID FROM Runtime WHERE ID = {ID} AND SourceTable='Device'", MetadataSynchronizationTimeout); - - if (sourceID is null || sourceID == DBNull.Value) - return; - - int parentID = Convert.ToInt32(sourceID); - - // Validate that the subscriber device is marked as a concentrator (we are about to associate children devices with it) - if (!command.ExecuteScalar($"SELECT IsConcentrator FROM Device WHERE ID = {parentID}", MetadataSynchronizationTimeout).ToString().ParseBoolean()) - command.ExecuteNonQuery($"UPDATE Device SET IsConcentrator = 1 WHERE ID = {parentID}", MetadataSynchronizationTimeout); - - // Get any historian associated with the subscriber device - object historianID = command.ExecuteScalar($"SELECT HistorianID FROM Device WHERE ID = {parentID}", MetadataSynchronizationTimeout); - - // Determine the active node ID - we cache this since this value won't change for the lifetime of this class - if (m_nodeID == Guid.Empty) - m_nodeID = Guid.Parse(command.ExecuteScalar($"SELECT NodeID FROM IaonInputAdapter WHERE ID = {(int)ID}", MetadataSynchronizationTimeout).ToString()); - - // Determine the protocol record auto-inc ID value for STTP - this value is also cached since it shouldn't change for the lifetime of this class - if (m_sttpProtocolID == 0) - m_sttpProtocolID = int.Parse(command.ExecuteScalar("SELECT ID FROM Protocol WHERE Acronym='STTP'", MetadataSynchronizationTimeout).ToString()); - - // Ascertain total number of actions required for all meta-data synchronization so some level feed back can be provided on progress - InitSyncProgress(metadata.Tables.Cast().Select(dataTable => (long)dataTable.Rows.Count).Sum() + 3); - - // Prefix all children devices with the name of the parent since the same device names could appear in different connections (helps keep device names unique) - string sourcePrefix = UseSourcePrefixNames ? $"{Name}!" : ""; - Dictionary deviceIDs = new(StringComparer.OrdinalIgnoreCase); - DateTime updateTime; - string deviceAcronym; - int deviceID; - - // Check to see if data for the "DeviceDetail" table was included in the meta-data - if (metadata.Tables.Contains("DeviceDetail")) - { - DataTable deviceDetail = metadata.Tables["DeviceDetail"]; - DataRow[] deviceRows; - - // Define SQL statement to query if this device is already defined (this should always be based on the unique guid-based device ID) - string deviceExistsSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Device WHERE UniqueID = {0}", "uniqueID"); - - // Define SQL statement to insert new device record - string insertDeviceSql = database.ParameterizedQueryString("INSERT INTO Device(NodeID, ParentID, HistorianID, Acronym, Name, ProtocolID, FramesPerSecond, OriginalSource, AccessID, Longitude, Latitude, ContactList, ConnectionString, IsConcentrator, Enabled) " + - "VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, 0, " + (SyncIndependentDevices ? AutoEnableIndependentlySyncedDevices ? "1" : "0" : "1") + ")", - "nodeID", "parentID", "historianID", "acronym", "name", "protocolID", "framesPerSecond", "originalSource", "accessID", "longitude", "latitude", "contactList", "connectionString"); - - // Define SQL statement to update device's guid-based unique ID after insert - string updateDeviceUniqueIDSql = database.ParameterizedQueryString("UPDATE Device SET UniqueID = {0} WHERE Acronym = {1}", "uniqueID", "acronym"); - - // Define SQL statement to query if a device can be safely updated - string deviceParentRestriction = SyncIndependentDevices ? "OriginalSource <> {1}" : "(ParentID <> {1} OR ParentID IS NULL)"; - string deviceIsUpdateableSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Device WHERE UniqueID = {0} AND " + deviceParentRestriction, "uniqueID", "parentID"); - - // Define SQL statement to update existing device record - string updateDeviceSql = database.ParameterizedQueryString("UPDATE Device SET Acronym = {0}, Name = {1}, OriginalSource = {2}, ProtocolID = {3}, FramesPerSecond = {4}, HistorianID = {5}, AccessID = {6}, Longitude = {7}, Latitude = {8}, ContactList = {9} WHERE UniqueID = {10}", - "acronym", "name", "originalSource", "protocolID", "framesPerSecond", "historianID", "accessID", "longitude", "latitude", "contactList", "uniqueID"); - - string updateDeviceWithConnectionStringSql = database.ParameterizedQueryString("UPDATE Device SET Acronym = {0}, Name = {1}, OriginalSource = {2}, ProtocolID = {3}, FramesPerSecond = {4}, HistorianID = {5}, AccessID = {6}, Longitude = {7}, Latitude = {8}, ContactList = {9}, ConnectionString = {10} WHERE UniqueID = {11}", - "acronym", "name", "originalSource", "protocolID", "framesPerSecond", "historianID", "accessID", "longitude", "latitude", "contactList", "connectionString", "uniqueID"); - - // Define SQL statement to retrieve device's auto-inc ID based on its unique guid-based ID - string queryDeviceIDSql = database.ParameterizedQueryString("SELECT ID FROM Device WHERE UniqueID = {0}", "uniqueID"); - - // Define SQL statement to retrieve all unique device ID's for the current parent to check for mismatches - string queryUniqueDeviceIDsSql = database.ParameterizedQueryString($"SELECT UniqueID FROM Device WHERE {(SyncIndependentDevices ? "OriginalSource" : "ParentID")} = {{0}}", "parentID"); - - // Define SQL statement to remove device records that no longer exist in the meta-data - string deleteDeviceSql = database.ParameterizedQueryString("DELETE FROM Device WHERE UniqueID = {0}", "uniqueID"); - - // Determine which device rows should be synchronized based on operational mode flags - if (ReceiveInternalMetadata && ReceiveExternalMetadata || MutualSubscription) - deviceRows = deviceDetail.Select(); - else if (ReceiveInternalMetadata) - deviceRows = deviceDetail.Select("OriginalSource IS NULL"); - else if (ReceiveExternalMetadata) - deviceRows = deviceDetail.Select("OriginalSource IS NOT NULL"); - else - deviceRows = []; - - // Check existence of optional meta-data fields - DataColumnCollection deviceDetailColumns = deviceDetail.Columns; - bool accessIDFieldExists = deviceDetailColumns.Contains("AccessID"); - bool longitudeFieldExists = deviceDetailColumns.Contains("Longitude"); - bool latitudeFieldExists = deviceDetailColumns.Contains("Latitude"); - bool companyAcronymFieldExists = deviceDetailColumns.Contains("CompanyAcronym"); - bool protocolNameFieldExists = deviceDetailColumns.Contains("ProtocolName"); - bool vendorAcronymFieldExists = deviceDetailColumns.Contains("VendorAcronym"); - bool vendorDeviceNameFieldExists = deviceDetailColumns.Contains("VendorDeviceName"); - bool interconnectionNameFieldExists = deviceDetailColumns.Contains("InterconnectionName"); - bool updatedOnFieldExists = deviceDetailColumns.Contains("UpdatedOn"); - bool connectionStringFieldExists = deviceDetailColumns.Contains("ConnectionString"); - object parentIDValue = SyncIndependentDevices ? parentID.ToString() : parentID; - int accessID = 0; - - List uniqueIDs = deviceRows - .Select(deviceRow => Guid.Parse(deviceRow.Field("UniqueID").ToString())) - .ToList(); - - // Remove any device records associated with this subscriber that no longer exist in the meta-data - if (uniqueIDs.Count > 0) - { - // ReSharper disable once AccessToDisposedClosure - IEnumerable retiredUniqueIDs = command - .RetrieveData(database.AdapterType, queryUniqueDeviceIDsSql, MetadataSynchronizationTimeout, parentIDValue) - .Select() - .Select(deviceRow => database.Guid(deviceRow, "UniqueID")) - .Except(uniqueIDs); - - foreach (Guid retiredUniqueID in retiredUniqueIDs) - command.ExecuteNonQuery(deleteDeviceSql, MetadataSynchronizationTimeout, database.Guid(retiredUniqueID)); - - UpdateSyncProgress(); - } - - foreach (DataRow row in deviceRows) - { - Guid uniqueID = Guid.Parse(row.Field("UniqueID").ToString()); - bool recordNeedsUpdating; - - // Determine if record has changed since last synchronization - if (updatedOnFieldExists) - { - try - { - updateTime = Convert.ToDateTime(row["UpdatedOn"]); - recordNeedsUpdating = updateTime > m_lastMetaDataRefreshTime; - - if (updateTime > latestUpdateTime) - latestUpdateTime = updateTime; - } - catch - { - recordNeedsUpdating = true; - } - } - else - { - recordNeedsUpdating = true; - } - - // We will synchronize meta-data only if the source owns this device, and it's not defined as a concentrator (these should normally be filtered by publisher - but we check just in case). - if (!row["IsConcentrator"].ToNonNullString("0").ParseBoolean()) - { - if (accessIDFieldExists) - accessID = row.ConvertField("AccessID"); - - // Get longitude and latitude values if they are defined - decimal longitude = 0M; - decimal latitude = 0M; - decimal? location; - string protocolName = null; - string connectionString = string.Empty; - - if (longitudeFieldExists) - { - location = row.ConvertNullableField("Longitude"); - - if (location.HasValue) - longitude = location.Value; - } - - if (latitudeFieldExists) - { - location = row.ConvertNullableField("Latitude"); - - if (location.HasValue) - latitude = location.Value; - } - - if (protocolNameFieldExists) - protocolName = row.Field("ProtocolName") ?? string.Empty; - - if (connectionStringFieldExists) - connectionString = row.Field("ConnectionString") ?? string.Empty; - - // Save any reported extraneous values from device meta-data in connection string formatted contact list - all fields are considered optional - Dictionary contactList = new(); - - if (companyAcronymFieldExists) - contactList["companyAcronym"] = row.Field("CompanyAcronym") ?? string.Empty; - - if (protocolNameFieldExists) - contactList["protocolName"] = protocolName; - - if (vendorAcronymFieldExists) - contactList["vendorAcronym"] = row.Field("VendorAcronym") ?? string.Empty; - - if (vendorDeviceNameFieldExists) - contactList["vendorDeviceName"] = row.Field("VendorDeviceName") ?? string.Empty; - - if (interconnectionNameFieldExists) - contactList["interconnectionName"] = row.Field("InterconnectionName") ?? string.Empty; - - int protocolID = m_sttpProtocolID; - - // If we are synchronizing independent devices, we need to determine the protocol ID for the device - // based on the protocol name defined in the meta-data - if (SyncIndependentDevices && !string.IsNullOrWhiteSpace(protocolName)) - { - string queryProtocolIDSql = database.ParameterizedQueryString("SELECT ID FROM Protocol WHERE Name = {0}", "protocolName"); - object protocolIDValue = command.ExecuteScalar(queryProtocolIDSql, MetadataSynchronizationTimeout, protocolName); - - if (protocolIDValue is not null && protocolIDValue is not DBNull) - protocolID = Convert.ToInt32(protocolIDValue); - - if (protocolID == 0) - protocolID = m_sttpProtocolID; - } - - // For mutual subscriptions where this subscription is owner (i.e., internal is true), we only sync devices that we did not provide - if (!MutualSubscription || !Internal || string.IsNullOrEmpty(row.Field("OriginalSource"))) - { - // Gateway is assuming ownership of the device records when the "internal" flag is true - this means the device's measurements can be forwarded to another party. From a device record perspective, - // ownership is inferred by setting 'OriginalSource' to null. When gateway doesn't own device records (i.e., the "internal" flag is false), this means the device's measurements can only be consumed - // locally - from a device record perspective this means the 'OriginalSource' field is set to the acronym of the PDC or PMU that generated the source measurements. This field allows a mirrored source - // restriction to be implemented later to ensure all devices in an output protocol came from the same original source connection, if desired. - object originalSource = SyncIndependentDevices ? parentID.ToString() : Internal ? DBNull.Value : - string.IsNullOrEmpty(row.Field("ParentAcronym")) ? - sourcePrefix + row.Field("Acronym") : - sourcePrefix + row.Field("ParentAcronym"); - - // Determine if device record already exists - if (Convert.ToInt32(command.ExecuteScalar(deviceExistsSql, MetadataSynchronizationTimeout, database.Guid(uniqueID))) == 0) - { - // Insert new device record - command.ExecuteNonQuery(insertDeviceSql, MetadataSynchronizationTimeout, database.Guid(m_nodeID), SyncIndependentDevices ? DBNull.Value : parentID, - historianID, sourcePrefix + row.Field("Acronym"), row.Field("Name"), protocolID, row.ConvertField("FramesPerSecond"), - originalSource, accessID, longitude, latitude, contactList.JoinKeyValuePairs(), connectionString); - - // Guids are normally auto-generated during insert - after insertion update the Guid so that it matches the source data. Most of the database - // scripts have triggers that support properly assigning the Guid during an insert, but this code ensures the Guid will always get assigned. - command.ExecuteNonQuery(updateDeviceUniqueIDSql, MetadataSynchronizationTimeout, database.Guid(uniqueID), sourcePrefix + row.Field("Acronym")); - } - else if (recordNeedsUpdating) - { - // Perform safety check to preserve device records which are not safe to overwrite (e.g., device already exists locally as part of another connection) - if (Convert.ToInt32(command.ExecuteScalar(deviceIsUpdateableSql, MetadataSynchronizationTimeout, database.Guid(uniqueID), parentIDValue)) > 0) - continue; - - // Update existing device record - if (connectionStringFieldExists) - command.ExecuteNonQuery(updateDeviceWithConnectionStringSql, MetadataSynchronizationTimeout, sourcePrefix + row.Field("Acronym"), row.Field("Name"), - originalSource, protocolID, row.ConvertField("FramesPerSecond"), historianID, accessID, longitude, latitude, contactList.JoinKeyValuePairs(), connectionString, database.Guid(uniqueID)); - else - command.ExecuteNonQuery(updateDeviceSql, MetadataSynchronizationTimeout, sourcePrefix + row.Field("Acronym"), row.Field("Name"), - originalSource, protocolID, row.ConvertField("FramesPerSecond"), historianID, accessID, longitude, latitude, contactList.JoinKeyValuePairs(), database.Guid(uniqueID)); - } - } - } - - // Capture local device ID auto-inc value for measurement association - deviceIDs[row.Field("Acronym")] = Convert.ToInt32(command.ExecuteScalar(queryDeviceIDSql, MetadataSynchronizationTimeout, database.Guid(uniqueID))); - - // Periodically notify user about synchronization progress - UpdateSyncProgress(); - } - } - - // Check to see if data for the "MeasurementDetail" table was included in the meta-data - if (metadata.Tables.Contains("MeasurementDetail")) - { - DataTable measurementDetail = metadata.Tables["MeasurementDetail"]; - List signalIDs = []; - DataRow[] measurementRows; - - // Define SQL statement to query if this measurement is already defined (this should always be based on the unique signal ID Guid) - string measurementExistsSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Measurement WHERE SignalID = {0}", "signalID"); - - // Define SQL statement to query if this measurement is already defined (this will be used before identity insert) - string identityMeasurementExistsSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Measurement WHERE PointID = {0}", "pointID"); - - // Define SQL statement to insert new measurement record - string insertMeasurementSql = database.ParameterizedQueryString("INSERT INTO Measurement(DeviceID, HistorianID, PointTag, AlternateTag, SignalTypeID, PhasorSourceIndex, SignalReference, Description, Internal, Subscribed, Enabled) " + - "VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, 0, 1)", "deviceID", "historianID", "pointTag", "tempAlternateTagID", "signalTypeID", "phasorSourceIndex", "signalReference", "description", "internal"); - - // Define SQL statement to insert new measurement record - string identityInsertMeasurementSql = database.ParameterizedQueryString("INSERT INTO Measurement(PointID, DeviceID, HistorianID, PointTag, AlternateTag, SignalTypeID, PhasorSourceIndex, SignalReference, Description, Internal, Subscribed, Enabled) " + - "VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, 0, 1)", "pointID", "deviceID", "historianID", "pointTag", "tempAlternateTagID", "signalTypeID", "phasorSourceIndex", "signalReference", "description", "internal"); - - // Define SQL statement to update measurement's signal ID after insert, restoring original signal ID and alternate tag from meta-data - string updateMeasurementSignalIDSql = database.ParameterizedQueryString("UPDATE Measurement SET SignalID = {0}, AlternateTag = {1} WHERE AlternateTag = {2}", "signalID", "alternateTag", "tempAlternateTagID"); - - // Define SQL statement to update existing measurement record - string updateMeasurementSql = database.ParameterizedQueryString("UPDATE Measurement SET HistorianID = {0}, PointTag = {1}, AlternateTag = {2}, SignalTypeID = {3}, PhasorSourceIndex = {4}, SignalReference = {5}, Description = {6}, Internal = {7} WHERE SignalID = {8}", - "historianID", "pointTag", "alternateTag", "signalTypeID", "phasorSourceIndex", "signalReference", "description", "internal", "signalID"); - - // Define SQL statement to update existing measurement record - string identityUpdateMeasurementSql = database.ParameterizedQueryString("UPDATE Measurement SET DeviceID = {0}, HistorianID = {1}, PointTag = {2}, AlternateTag = {3}, SignalTypeID = {4}, PhasorSourceIndex = {5}, SignalReference = {6}, Description = {7}, Internal = {8}, Subscribed = 0, Enabled = 1, SignalID = {9} WHERE PointID = {10}", - "deviceID", "historianID", "pointTag", "tempAlternateTagID", "signalTypeID", "phasorSourceIndex", "signalReference", "description", "internal", "signalID", "pointID"); - - // Define SQL statement to retrieve all measurement signal ID's for the current parent to check for mismatches - note that we use the ActiveMeasurements view - // since it associates measurements with their top-most parent runtime device ID, this allows us to easily query all measurements for the parent device - string queryMeasurementSignalIDsSql = database.ParameterizedQueryString("SELECT SignalID FROM ActiveMeasurement WHERE DeviceID = {0}", "deviceID"); - - // Define SQL statement to retrieve measurement's associated device ID, i.e., actual record ID, based on measurement's signal ID - string queryMeasurementDeviceIDSql = database.ParameterizedQueryString("SELECT DeviceID FROM Measurement WHERE SignalID = {0}", "signalID"); - - // Load signal type ID's from local database associated with their acronym for proper signal type translation - Dictionary signalTypeIDs = new(StringComparer.OrdinalIgnoreCase); - - string signalTypeAcronym; - - foreach (DataRow row in command.RetrieveData(database.AdapterType, "SELECT ID, Acronym FROM SignalType", MetadataSynchronizationTimeout).Rows) - { - signalTypeAcronym = row.Field("Acronym"); - - if (!string.IsNullOrWhiteSpace(signalTypeAcronym)) - signalTypeIDs[signalTypeAcronym] = row.ConvertField("ID"); - } - - // Define local signal type ID deletion exclusion set - string deleteCondition = ""; - - if (MutualSubscription && !Internal) - { - // For mutual subscriptions where this subscription is renter (i.e., internal is false), do not delete measurements that are locally owned - deleteCondition = " AND Internal == 0"; - } - else - { - List excludedSignalTypeIDs = []; - - // We are intentionally ignoring CALC and ALRM signals during measurement deletion since if you have subscribed to a device and subsequently created local - // calculations and alarms associated with this device, these signals are locally owned and not part of the publisher subscription stream. As a result any - // CALC or ALRM measurements that are created at source and then removed could be orphaned in subscriber. The best fix would be to have a simple flag that - // clearly designates that a measurement was created locally and is not part of the remote synchronization set. - if (!AutoDeleteCalculatedMeasurements && signalTypeIDs.TryGetValue("CALC", out int signalTypeID)) - excludedSignalTypeIDs.Add(signalTypeID); - - if (!AutoDeleteAlarmMeasurements && signalTypeIDs.TryGetValue("ALRM", out signalTypeID)) - excludedSignalTypeIDs.Add(signalTypeID); - - if (excludedSignalTypeIDs.Count > 0) - deleteCondition = $" AND NOT SignalTypeID IN ({excludedSignalTypeIDs.ToDelimitedString(',')})"; - } - - // Define SQL statement to remove device records that no longer exist in the meta-data - string deleteMeasurementSql = database.ParameterizedQueryString($"DELETE FROM Measurement WHERE SignalID = {{0}}{deleteCondition}", "signalID"); - - // Determine which measurement rows should be synchronized based on operational mode flags - if (ReceiveInternalMetadata && ReceiveExternalMetadata) - measurementRows = measurementDetail.Select(); - else if (ReceiveInternalMetadata) - measurementRows = measurementDetail.Select("Internal <> 0"); - else if (ReceiveExternalMetadata) - measurementRows = measurementDetail.Select("Internal = 0"); - else - measurementRows = []; - - // Check existence of optional meta-data fields - DataColumnCollection measurementDetailColumns = measurementDetail.Columns; - bool phasorSourceIndexFieldExists = measurementDetailColumns.Contains("PhasorSourceIndex"); - bool updatedOnFieldExists = measurementDetailColumns.Contains("UpdatedOn"); - bool alternateTagFieldExists = measurementDetailColumns.Contains("AlternateTag"); - - object phasorSourceIndex = DBNull.Value; - object alternateTag = DBNull.Value; - - if (UseIdentityInsertsForMetadata && database.IsSQLServer) - command.ExecuteNonQuery("SET IDENTITY_INSERT Measurement ON"); - - try - { - - foreach (DataRow row in measurementRows) - { - bool recordNeedsUpdating; - - // Determine if record has changed since last synchronization - if (updatedOnFieldExists) - { - try - { - updateTime = Convert.ToDateTime(row["UpdatedOn"]); - recordNeedsUpdating = updateTime > m_lastMetaDataRefreshTime; - - if (updateTime > latestUpdateTime) - latestUpdateTime = updateTime; - } - catch - { - recordNeedsUpdating = true; - } - } - else - { - recordNeedsUpdating = true; - } - - // Get device and signal type acronyms - deviceAcronym = row.Field("DeviceAcronym") ?? string.Empty; - signalTypeAcronym = row.Field("SignalAcronym") ?? string.Empty; - - // Get phasor source index if field is defined - if (phasorSourceIndexFieldExists) - { - // Using ConvertNullableField extension since publisher could use SQLite database in which case - // all integers would arrive in data set as longs and need to be converted back to integers - int? index = row.ConvertNullableField("PhasorSourceIndex"); - phasorSourceIndex = index ?? (object)DBNull.Value; - } - - // Get alternate tag if field is defined - if (alternateTagFieldExists) - alternateTag = row.Field("AlternateTag") ?? (object)DBNull.Value; - - // Make sure we have an associated device and signal type already defined for the measurement - if (!string.IsNullOrWhiteSpace(deviceAcronym) && deviceIDs.ContainsKey(deviceAcronym) && !string.IsNullOrWhiteSpace(signalTypeAcronym) && signalTypeIDs.ContainsKey(signalTypeAcronym)) - { - Guid signalID = Guid.Parse(row.Field("SignalID").ToString()); - - // Track unique measurement signal Guids in this meta-data session, we'll need to remove any old associated measurements that no longer exist - signalIDs.Add(signalID); - - // Prefix the tag name with the "updated" device name - string pointTag = sourcePrefix + row.Field("PointTag"); - - // Look up associated device ID (local DB auto-inc) - deviceID = deviceIDs[deviceAcronym]; - - // Determine if measurement record already exists - if (Convert.ToInt32(command.ExecuteScalar(measurementExistsSql, MetadataSynchronizationTimeout, database.Guid(signalID))) == 0) - { - string temporaryAlternateTagID = Guid.NewGuid().ToString(); - - // Insert new measurement record - if (UseIdentityInsertsForMetadata && MeasurementKey.TryParse(row.Field("ID"), out MeasurementKey measurementKey)) - { - long pointID = (long)measurementKey.ID; - - if (Convert.ToInt32(command.ExecuteScalar(identityMeasurementExistsSql, MetadataSynchronizationTimeout, pointID)) == 0) - command.ExecuteNonQuery(identityInsertMeasurementSql, MetadataSynchronizationTimeout, pointID, deviceID, historianID, pointTag, temporaryAlternateTagID, signalTypeIDs[signalTypeAcronym], phasorSourceIndex, sourcePrefix + row.Field("SignalReference"), row.Field("Description") ?? string.Empty, database.Bool(Internal)); - else - command.ExecuteNonQuery(identityUpdateMeasurementSql, MetadataSynchronizationTimeout, deviceID, historianID, pointTag, temporaryAlternateTagID, signalTypeIDs[signalTypeAcronym], phasorSourceIndex, sourcePrefix + row.Field("SignalReference"), row.Field("Description") ?? string.Empty, database.Bool(Internal), database.Guid(signalID), pointID); - } - else - { - command.ExecuteNonQuery(insertMeasurementSql, MetadataSynchronizationTimeout, deviceID, historianID, pointTag, temporaryAlternateTagID, signalTypeIDs[signalTypeAcronym], phasorSourceIndex, sourcePrefix + row.Field("SignalReference"), row.Field("Description") ?? string.Empty, database.Bool(Internal)); - } - - // Guids are normally auto-generated during insert - after insertion update the Guid so that it matches the source data. Most of the database - // scripts have triggers that support properly assigning the Guid during an insert, but this code ensures the Guid will always get assigned. - // TODO: Ensure database schemas define an index on the AlternateTag field to optimize this update - command.ExecuteNonQuery(updateMeasurementSignalIDSql, MetadataSynchronizationTimeout, database.Guid(signalID), alternateTag, temporaryAlternateTagID); - } - else if (recordNeedsUpdating) - { - // Update existing measurement record. Note that this update assumes that measurements will remain associated with a static source device. - command.ExecuteNonQuery(updateMeasurementSql, MetadataSynchronizationTimeout, historianID, pointTag, alternateTag, signalTypeIDs[signalTypeAcronym], phasorSourceIndex, sourcePrefix + row.Field("SignalReference"), row.Field("Description") ?? string.Empty, database.Bool(Internal), database.Guid(signalID)); - } - } - - // Periodically notify user about synchronization progress - UpdateSyncProgress(); - } - } - finally - { - if (UseIdentityInsertsForMetadata && database.IsSQLServer) - command.ExecuteNonQuery("SET IDENTITY_INSERT Measurement OFF"); - } - - // Remove any measurement records associated with existing devices in this session but no longer exist in the meta-data - if (signalIDs.Count > 0) - { - // Sort signal ID list so that binary search can be used for quick lookups - signalIDs.Sort(); - - // Query all the guid-based signal ID's for all measurement records associated with the parent device using run-time ID - DataTable measurementSignalIDs = command.RetrieveData(database.AdapterType, queryMeasurementSignalIDsSql, MetadataSynchronizationTimeout, (int)ID); - - // Walk through each database record and see if the measurement exists in the provided meta-data - foreach (DataRow measurementRow in measurementSignalIDs.Rows) - { - Guid signalID = database.Guid(measurementRow, "SignalID"); - - // Remove any measurements in the database that are associated with received devices and do not exist in the meta-data - if (signalIDs.BinarySearch(signalID) >= 0) - continue; - - // Measurement was not in the meta-data, get the measurement's actual record based ID for its associated device - object measurementDeviceID = command.ExecuteScalar(queryMeasurementDeviceIDSql, MetadataSynchronizationTimeout, database.Guid(signalID)); - - // If the unknown measurement is directly associated with a device that exists in the meta-data it is assumed that this measurement - // was removed from the publishing system and no longer exists therefore we remove it from the local measurement cache. If the user - // needs custom local measurements associated with a remote device, they should be associated with the parent device only. - if (measurementDeviceID is not null && measurementDeviceID is not DBNull && deviceIDs.ContainsValue(Convert.ToInt32(measurementDeviceID))) - command.ExecuteNonQuery(deleteMeasurementSql, MetadataSynchronizationTimeout, database.Guid(signalID)); - } - - UpdateSyncProgress(); - } - } - - // Check to see if data for the "PhasorDetail" table was included in the meta-data - if (metadata.Tables.Contains("PhasorDetail")) - { - DataTable phasorDetail = metadata.Tables["PhasorDetail"]; - Dictionary> definedSourceIndices = new(); - Dictionary metadataToDatabaseIDMap = new(); - Dictionary sourceToDestinationIDMap = new(); - - // Phasor data is normally only needed so that the user can properly generate a mirrored IEEE C37.118 output stream from the source data. - // This is necessary since, in this protocol, the phasors are described (i.e., labeled) as a unit (i.e., as a complex number) instead of - // as two distinct angle and magnitude measurements. - - // Define SQL statement to query if phasor record is already defined (no Guid is defined for these simple label records) - string phasorExistsSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Phasor WHERE DeviceID = {0} AND SourceIndex = {1}", "deviceID", "sourceIndex"); - - // Define SQL statement to insert new phasor record - string insertPhasorSql = database.ParameterizedQueryString("INSERT INTO Phasor(DeviceID, Label, Type, Phase, SourceIndex) VALUES ({0}, {1}, {2}, {3}, {4})", "deviceID", "label", "type", "phase", "sourceIndex"); - - // Define SQL statement to update existing phasor record - string updatePhasorSql = database.ParameterizedQueryString("UPDATE Phasor SET Label = {0}, Type = {1}, Phase = {2} WHERE DeviceID = {3} AND SourceIndex = {4}", "label", "type", "phase", "deviceID", "sourceIndex"); - - // Define SQL statement to delete a phasor record - string deletePhasorSql = database.ParameterizedQueryString("DELETE FROM Phasor WHERE DeviceID = {0}", "deviceID"); - - // Define SQL statement to query phasor record ID - string queryPhasorIDSql = database.ParameterizedQueryString("SELECT ID FROM Phasor WHERE DeviceID = {0} AND SourceIndex = {1}", "deviceID", "sourceIndex"); - - // Define SQL statement to update destinationPhasorID field of existing phasor record - string updateDestinationPhasorIDSql = database.ParameterizedQueryString("UPDATE Phasor SET DestinationPhasorID = {0} WHERE ID = {1}", "destinationPhasorID", "id"); - - // Define SQL statement to update phasor BaseKV - string updatePhasorBaseKVSql = database.ParameterizedQueryString("UPDATE Phasor SET BaseKV = {0} WHERE DeviceID = {1} AND SourceIndex = {2}", "baseKV", "deviceID", "sourceIndex"); - - // Check existence of optional meta-data fields - DataColumnCollection phasorDetailColumns = phasorDetail.Columns; - bool phasorIDFieldExists = phasorDetailColumns.Contains("ID"); - bool destinationPhasorIDFieldExists = phasorDetailColumns.Contains("DestinationPhasorID"); - bool baseKVFieldExists = phasorDetailColumns.Contains("BaseKV"); - - foreach (DataRow row in phasorDetail.Rows) - { - // Get device acronym - deviceAcronym = row.Field("DeviceAcronym") ?? string.Empty; - - // Make sure we have an associated device already defined for the phasor record - // ReSharper disable once CanSimplifyDictionaryLookupWithTryGetValue - if (!string.IsNullOrWhiteSpace(deviceAcronym) && deviceIDs.ContainsKey(deviceAcronym)) - { - bool recordNeedsUpdating; - - // Determine if record has changed since last synchronization - try - { - updateTime = Convert.ToDateTime(row["UpdatedOn"]); - recordNeedsUpdating = updateTime > m_lastMetaDataRefreshTime; - - if (updateTime > latestUpdateTime) - latestUpdateTime = updateTime; - } - catch - { - recordNeedsUpdating = true; - } - - deviceID = deviceIDs[deviceAcronym]; - - int sourceIndex = row.ConvertField("SourceIndex"); - bool updateRecord = false; - - // Determine if phasor record already exists - if (Convert.ToInt32(command.ExecuteScalar(phasorExistsSql, MetadataSynchronizationTimeout, deviceID, sourceIndex)) == 0) - { - // Insert new phasor record - command.ExecuteNonQuery(insertPhasorSql, MetadataSynchronizationTimeout, deviceID, row.Field("Label") ?? "undefined", (row.Field("Type") ?? "V").TruncateLeft(1), (row.Field("Phase") ?? "+").TruncateLeft(1), sourceIndex); - updateRecord = true; - } - else if (recordNeedsUpdating) - { - // Update existing phasor record - command.ExecuteNonQuery(updatePhasorSql, MetadataSynchronizationTimeout, row.Field("Label") ?? "undefined", (row.Field("Type") ?? "V").TruncateLeft(1), (row.Field("Phase") ?? "+").TruncateLeft(1), deviceID, sourceIndex); - updateRecord = true; - } - - if (updateRecord && baseKVFieldExists) - command.ExecuteNonQuery(updatePhasorBaseKVSql, MetadataSynchronizationTimeout, row.ConvertField("BaseKV"), deviceID, sourceIndex); - - if (phasorIDFieldExists && destinationPhasorIDFieldExists) - { - int sourcePhasorID = row.ConvertField("ID"); - - // Using ConvertNullableField extension since publisher could use SQLite database in which case - // all integers would arrive in data set as longs and need to be converted back to integers - int? destinationPhasorID = row.ConvertNullableField("DestinationPhasorID"); - - if (destinationPhasorID.HasValue) - sourceToDestinationIDMap[sourcePhasorID] = destinationPhasorID.Value; - - // Map all metadata phasor IDs to associated local database phasor IDs - metadataToDatabaseIDMap[sourcePhasorID] = Convert.ToInt32(command.ExecuteScalar(queryPhasorIDSql, MetadataSynchronizationTimeout, deviceID, sourceIndex)); - } - - // Track defined phasors for each device - definedSourceIndices.GetOrAdd(deviceID, _ => []).Add(sourceIndex); - } - - // Periodically notify user about synchronization progress - UpdateSyncProgress(); - } - - // Once all phasor records have been processed, handle updating of destination phasor IDs - foreach (KeyValuePair item in sourceToDestinationIDMap) - { - if (metadataToDatabaseIDMap.TryGetValue(item.Key, out int sourcePhasorID) && metadataToDatabaseIDMap.TryGetValue(item.Value, out int destinationPhasorID)) - command.ExecuteNonQuery(updateDestinationPhasorIDSql, MetadataSynchronizationTimeout, destinationPhasorID, sourcePhasorID); - } - - // For mutual subscriptions where this subscription is owner (i.e., internal is true), do not delete any phasor data - it will be managed by owner only - if (!MutualSubscription || !Internal) - { - // Remove any phasor records associated with existing devices in this session but no longer exist in the meta-data - foreach (int id in deviceIDs.Values) - { - string deleteSql = definedSourceIndices.TryGetValue(id, out List sourceIndices) ? - $"{deletePhasorSql} AND SourceIndex NOT IN ({string.Join(",", sourceIndices)})" : - deletePhasorSql; - - command.ExecuteNonQuery(deleteSql, MetadataSynchronizationTimeout, id); - } - } - } - - transaction?.Commit(); - - // Update local in-memory synchronized meta-data cache - m_synchronizedMetadata = metadata; - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to synchronize meta-data to local cache: {ex.Message}", ex)); - - if (transaction is not null) - { - try - { - transaction.Rollback(); - } - catch (Exception rollbackException) - { - OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to roll back database transaction due to exception: {rollbackException.Message}", rollbackException)); - } - } - - return; - } - finally - { - transaction?.Dispose(); - } - } - - // New signals may have been defined, take original remote signal index cache and apply changes - if (m_remoteSignalIndexCache is not null && m_signalIndexCache is not null) - { - SignalIndexCache originalReference, remoteSignalIndexCache; - - lock (m_signalIndexCacheLock) - { - originalReference = m_signalIndexCache[m_cacheIndex]; - remoteSignalIndexCache = m_remoteSignalIndexCache; - } - - SignalIndexCache signalIndexCache = new(DataSource, remoteSignalIndexCache); - - if (signalIndexCache.Reference.Count > 0) - { - lock (m_signalIndexCacheLock) - { - if (ReferenceEquals(originalReference, m_signalIndexCache[m_cacheIndex])) - { - signalIndexCache.TsscDecoder = m_signalIndexCache[m_cacheIndex].TsscDecoder; - m_signalIndexCache[m_cacheIndex] = signalIndexCache; - } - } - } - } - - m_lastMetaDataRefreshTime = latestUpdateTime > DateTime.MinValue ? latestUpdateTime : DateTime.UtcNow; - - OnStatusMessage(MessageLevel.Info, $"Meta-data synchronization completed successfully in {(DateTime.UtcNow.Ticks - startTime).ToElapsedTimeString(2)}"); - - // Send notification that system configuration has changed - OnConfigurationChanged(); - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to synchronize meta-data to local cache: {ex.Message}", ex)); - } - finally - { - // Restart data stream monitor after meta-data synchronization if it was originally enabled - if (dataMonitoringEnabled && m_dataStreamMonitor is not null) - m_dataStreamMonitor.Enabled = true; - } - } - - private void InitSyncProgress(long totalActions) - { - m_syncProgressTotalActions = totalActions; - m_syncProgressActionsCount = 0; - m_syncProgressLastMessage = DateTime.UtcNow.Ticks; - } - - private void UpdateSyncProgress() - { - m_syncProgressActionsCount++; - - // We update user on progress every 15 seconds or at 100% complete - if (DateTime.UtcNow.Ticks - m_syncProgressLastMessage < 150000000 && m_syncProgressActionsCount < m_syncProgressTotalActions) - return; - - OnStatusMessage(MessageLevel.Info, $"Meta-data synchronization is {m_syncProgressActionsCount / (double)m_syncProgressTotalActions:0.0%} complete..."); - m_syncProgressLastMessage = DateTime.UtcNow.Ticks; - } - - private SignalIndexCache DeserializeSignalIndexCache(byte[] buffer) - { - CompressionModes compressionModes = (CompressionModes)(m_operationalModes & OperationalModes.CompressionModeMask); - bool compressSignalIndexCache = (m_operationalModes & OperationalModes.CompressSignalIndexCache) > 0; - GZipStream inflater = null; - - if (compressSignalIndexCache && compressionModes.HasFlag(CompressionModes.GZip)) - { - try - { - using MemoryStream compressedData = new(buffer); - inflater = new GZipStream(compressedData, CompressionMode.Decompress, true); - buffer = inflater.ReadStream(); - } - finally - { - inflater?.Close(); - } - } - - SignalIndexCache deserializedCache = new() { Encoding = Encoding }; - deserializedCache.ParseBinaryImage(buffer, 0, buffer.Length); - - return deserializedCache; - } - - private DataSet DeserializeMetadata(byte[] buffer) - { - CompressionModes compressionModes = (CompressionModes)(m_operationalModes & OperationalModes.CompressionModeMask); - bool compressMetadata = (m_operationalModes & OperationalModes.CompressMetadata) > 0; - Ticks startTime = DateTime.UtcNow.Ticks; - DataSet deserializedMetadata; - GZipStream inflater = null; - - if (compressMetadata && compressionModes.HasFlag(CompressionModes.GZip)) - { - try - { - // Insert compressed data into compressed buffer - using MemoryStream compressedData = new(buffer); - inflater = new GZipStream(compressedData, CompressionMode.Decompress, true); - buffer = inflater.ReadStream(); - } - finally - { - inflater?.Close(); - } - } - - // Copy decompressed data into encoded buffer - using (MemoryStream encodedData = new(buffer)) - using (XmlTextReader xmlReader = new(encodedData)) - { - // Read encoded data into data set as XML - deserializedMetadata = new DataSet(); - deserializedMetadata.ReadXml(xmlReader, XmlReadMode.ReadSchema); - } - - long rowCount = deserializedMetadata.Tables.Cast().Select(dataTable => (long)dataTable.Rows.Count).Sum(); - - if (rowCount > 0) - { - Time elapsedTime = (DateTime.UtcNow.Ticks - startTime).ToSeconds(); - OnStatusMessage(MessageLevel.Info, $"Received a total of {rowCount:N0} records spanning {deserializedMetadata.Tables.Count:N0} tables of meta-data that was {(compressMetadata ? "uncompressed and " : "")}deserialized in {elapsedTime.ToString(3)}..."); - } - - return deserializedMetadata; - } - - private static Encoding GetCharacterEncoding(OperationalEncoding operationalEncoding) - { - Encoding encoding = operationalEncoding switch - { - OperationalEncoding.UTF16LE => Encoding.Unicode, - OperationalEncoding.UTF16BE => Encoding.BigEndianUnicode, - OperationalEncoding.UTF8 => Encoding.UTF8, - _ => throw new InvalidOperationException($"Unsupported encoding detected: {operationalEncoding}") - }; - - return encoding; - } - - // Socket exception handler - private bool HandleSocketException(Exception ex) - { - // WSAECONNABORTED and WSAECONNRESET are common errors after a client disconnect, - // if they happen for other reasons, make sure disconnect procedure is handled - if (ex is SocketException { ErrorCode: 10053 or 10054 }) - { - DisconnectClient(); - return true; - } - - if (ex is not null) - HandleSocketException(ex.InnerException); - - return false; - } - - // Disconnect client, restarting if disconnect was not intentional - private void DisconnectClient() - { - // Mark end of any data transmission in run-time log - if (m_runTimeLog is not null && m_runTimeLog.Enabled) - { - m_runTimeLog.StopTime = DateTimeOffset.UtcNow; - m_runTimeLog.Enabled = false; - } - - // Stop data gap recovery operations - if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null) - { - try - { - m_dataGapRecoverer.Enabled = false; - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Exception while attempting to flush data gap recoverer log: {ex.Message}", ex)); - } - } - - DataChannel = null; - m_metadataRefreshPending = false; - - if (m_serverCommandChannel is null) - { - // If user didn't initiate disconnect, restart the connection cycle - if (Enabled) - Start(); - } - else - { - if (m_activeClientID != Guid.Empty) - m_serverCommandChannel.DisconnectOne(m_activeClientID); - - // When subscriber is in server mode, the server does not need to be restarted, but we - // will reset client statistics - this is a server of "one" client, the publisher - m_activeClientID = Guid.Empty; - m_subscribedDevicesTimer?.Stop(); - m_metadataRefreshPending = false; - m_expectedBufferBlockSequenceNumber = 0u; - m_subscribed = false; - m_keyIVs = null; - TotalBytesReceived = 0L; - m_lastBytesReceived = 0; - m_lastReceivedAt = DateTime.MinValue; - } - } - - // Gets the socket instance used by this client - private Socket GetCommandChannelSocket() - { - Guid clientID = m_serverCommandChannel?.ClientIDs.FirstOrDefault() ?? Guid.Empty; - - return m_serverCommandChannel switch - { - TcpServer tcpServerCommandChannel when tcpServerCommandChannel.TryGetClient(clientID, out TransportProvider tcpProvider) => tcpProvider.Provider, - TlsServer tlsServerCommandChannel when tlsServerCommandChannel.TryGetClient(clientID, out TransportProvider tlsProvider) => tlsProvider.Provider?.Socket, - _ => (m_clientCommandChannel as TcpClient)?.Client ?? (m_clientCommandChannel as TcpSimpleClient)?.Client - }; - } - - private void HandleDeviceStatisticsRegistration() - { - if (BypassStatistics) - return; - - if (Enabled) - RegisterDeviceStatistics(); - else - UnregisterDeviceStatistics(); - } - - private void RegisterDeviceStatistics() - { - long now = UseLocalClockAsRealTime ? DateTime.UtcNow.Ticks : 0L; - - try - { - DataSet dataSource = DataSource; - - if (dataSource is null || !dataSource.Tables.Contains("InputStreamDevices")) - { - if (m_statisticsHelpers is not null) - { - foreach (DeviceStatisticsHelper statisticsHelper in m_statisticsHelpers) - statisticsHelper.Device.Dispose(); - } - - m_statisticsHelpers = []; - m_subscribedDevicesLookup = new Dictionary>(); - } - else - { - Dictionary> subscribedDevicesLookup = new(); - List> subscribedDevices = []; - ISet subscribedDeviceNames = new HashSet(); - ISet definedDeviceNames = new HashSet(); - - foreach (DataRow deviceRow in dataSource.Tables["InputStreamDevices"].Select($"ParentID = {ID}")) - definedDeviceNames.Add($"LOCAL${deviceRow["Acronym"].ToNonNullString()}"); - - if (m_statisticsHelpers is not null) - { - foreach (DeviceStatisticsHelper statisticsHelper in m_statisticsHelpers) - { - if (definedDeviceNames.Contains(statisticsHelper.Device.Name)) - { - subscribedDevices.Add(statisticsHelper); - subscribedDeviceNames.Add(statisticsHelper.Device.Name); - } - else - { - statisticsHelper.Device.Dispose(); - } - } - } - - foreach (string definedDeviceName in definedDeviceNames) - { - if (subscribedDeviceNames.Contains(definedDeviceName)) - continue; - - DeviceStatisticsHelper statisticsHelper = new(new SubscribedDevice(definedDeviceName)); - subscribedDevices.Add(statisticsHelper); - statisticsHelper.Reset(now); - } - - if (dataSource.Tables.Contains("ActiveMeasurements")) - { - ActiveMeasurementsTableLookup measurementLookup = DataSourceLookups.ActiveMeasurements(dataSource); - - foreach (DeviceStatisticsHelper statisticsHelper in subscribedDevices) - { - string deviceName = Regex.Replace(statisticsHelper.Device.Name, @"^LOCAL\$", ""); - - foreach (DataRow measurementRow in measurementLookup.LookupByDeviceNameNoStat(deviceName)) - { - if (Guid.TryParse(measurementRow["SignalID"].ToNonNullString(), out Guid signalID)) - { - // In some rare cases duplicate signal ID's have been encountered (likely bad configuration), - // as a result we use a GetOrAdd instead of an Add - subscribedDevicesLookup.GetOrAdd(signalID, statisticsHelper); - - switch (measurementRow["SignalType"].ToNonNullString()) - { - case "FLAG": - statisticsHelper.Device.StatusFlagsID = signalID; - break; - - case "FREQ": - statisticsHelper.Device.FrequencyID = signalID; - break; - - case "DFDT": - statisticsHelper.Device.DeltaFrequencyID = signalID; - break; - } - } - } - } - } - - m_subscribedDevicesLookup = subscribedDevicesLookup; - m_statisticsHelpers = subscribedDevices; - } - - FixExpectedMeasurementCounts(); - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Unable to register device statistics due to exception: {ex.Message}", ex)); - } - } - - private void UnregisterDeviceStatistics() - { - try - { - if (m_statisticsHelpers is null) - return; - - foreach (DeviceStatisticsHelper statisticsHelper in m_statisticsHelpers) - statisticsHelper.Device.Dispose(); - - m_statisticsHelpers = []; - m_subscribedDevicesLookup = new Dictionary>(); - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Unable to unregister device statistics due to exception: {ex.Message}", ex)); - } - } - - private void FixExpectedMeasurementCounts() - { - Dictionary> subscribedDevicesLookup = m_subscribedDevicesLookup; - List> statisticsHelpers = m_statisticsHelpers; - DataSet dataSource = DataSource; - SignalIndexCache signalIndexCache; - DataTable measurementTable; - - lock (m_signalIndexCacheLock) - signalIndexCache = m_signalIndexCache?[m_cacheIndex]; - - try - { - if (statisticsHelpers is null || subscribedDevicesLookup is null) - return; - - if (signalIndexCache is null) - return; - - if (dataSource is null || !dataSource.Tables.Contains("ActiveMeasurements")) - return; - - measurementTable = dataSource.Tables["ActiveMeasurements"]; - - if (!measurementTable.Columns.Contains("FramesPerSecond")) - return; - - // Get expected measurement counts - IEnumerable, Guid>> groups = signalIndexCache.AuthorizedSignalIDs - .Where(signalID => subscribedDevicesLookup.TryGetValue(signalID, out _)) - .Select(signalID => Tuple.Create(subscribedDevicesLookup[signalID], signalID)) - .ToList() - .GroupBy(tuple => tuple.Item1, tuple => tuple.Item2); - - foreach (IGrouping, Guid> group in groups) - { - int[] frameRates = group - .Select(signalID => GetFramesPerSecond(measurementTable, signalID)) - .Where(frameRate => frameRate != 0) - .ToArray(); - - group.Key.Device.MeasurementsDefined = frameRates.Length; - group.Key.ExpectedMeasurementsPerSecond = frameRates.Sum(); - } - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Unable to set expected measurement counts for gathering statistics due to exception: {ex.Message}", ex)); - } - } - - private int GetFramesPerSecond(DataTable measurementTable, Guid signalID) - { - DataRow row = measurementTable.Select($"SignalID = '{signalID}'").FirstOrDefault(); - - if (row is null) - return 0; - - return row.Field("SignalType").ToUpperInvariant() switch - { - "FLAG" => 0, - "STAT" => 0, - "CALC" => 0, - "ALRM" => 0, - "QUAL" => 0, - _ => row.ConvertField("FramesPerSecond") - }; - } - - // This method is called when connection has been authenticated - private void DataSubscriber_ConnectionAuthenticated(object sender, EventArgs e) - { - if (AutoConnect && Enabled) - StartSubscription(); - } - - // This method is called then new meta-data has been received - private void DataSubscriber_MetaDataReceived(object sender, EventArgs e) - { - try - { - // We handle synchronization on a separate thread since this process may be lengthy - if (AutoSynchronizeMetadata) - SynchronizeMetadata(e.Argument); - } - catch (Exception ex) - { - // Process exception for logging - OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to queue meta-data synchronization due to exception: {ex.Message}", ex)); - } - } - - /// - /// Raises the event. - /// - protected void OnConnectionEstablished() - { - try - { - ConnectionEstablished?.Invoke(this, EventArgs.Empty); - m_lastMissingCacheWarning = 0L; - } - catch (Exception ex) - { - // We protect our code from consumer thrown exceptions - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ConnectionEstablished event: {ex.Message}", ex), "ConsumerEventException"); - } - } - - /// - /// Raises the event. - /// - protected void OnConnectionTerminated() - { - try - { - ConnectionTerminated?.Invoke(this, EventArgs.Empty); - } - catch (Exception ex) - { - // We protect our code from consumer thrown exceptions - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ConnectionTerminated event: {ex.Message}", ex), "ConsumerEventException"); - } - } - - /// - /// Raises the event. - /// - protected void OnConnectionAuthenticated() - { - try - { - ConnectionAuthenticated?.Invoke(this, EventArgs.Empty); - } - catch (Exception ex) - { - // We protect our code from consumer thrown exceptions - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ConnectionAuthenticated event: {ex.Message}", ex), "ConsumerEventException"); - } - } - - /// - /// Raises the event. - /// - /// Response received from the server. - /// Command that the server responded to. - protected void OnReceivedServerResponse(ServerResponse responseCode, ServerCommand commandCode) - { - try - { - ReceivedServerResponse?.Invoke(this, new EventArgs(responseCode, commandCode)); - } - catch (Exception ex) - { - // We protect our code from consumer thrown exceptions - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ReceivedServerResponse event: {ex.Message}", ex), "ConsumerEventException"); - } - } - - /// - /// Raises the event. - /// - /// The code for the user command. - /// The code for the server's response. - /// Buffer containing the message from the server. - /// Index into the buffer used to skip the header. - /// The length of the message in the buffer, including the header. - protected void OnReceivedUserCommandResponse(ServerCommand command, ServerResponse response, byte[] buffer, int startIndex, int length) - { - try - { - UserCommandArgs args = new(command, response, buffer, startIndex, length); - ReceivedUserCommandResponse?.Invoke(this, args); - } - catch (Exception ex) - { - // We protect our code from consumer thrown exceptions - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for UserCommandResponse event: {ex.Message}", ex), "ConsumerEventException"); - } - } - - /// - /// Raises the event. - /// - /// Meta-data instance to send to client subscription. - protected void OnMetaDataReceived(DataSet metadata) - { - try - { - MetaDataReceived?.Invoke(this, new EventArgs(metadata)); - } - catch (Exception ex) - { - // We protect our code from consumer thrown exceptions - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for MetaDataReceived event: {ex.Message}", ex), "ConsumerEventException"); - } - } - - /// - /// Raises the event. - /// - /// Start time, in , of first measurement transmitted. - protected void OnDataStartTime(Ticks startTime) - { - try - { - DataStartTime?.Invoke(this, new EventArgs(startTime)); - } - catch (Exception ex) - { - // We protect our code from consumer thrown exceptions - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for DataStartTime event: {ex.Message}", ex), "ConsumerEventException"); - } - } - - /// - /// Raises the event. - /// - /// Type name of adapter that sent the processing completed notification. - protected void OnProcessingComplete(string source) - { - try - { - ProcessingComplete?.Invoke(this, new EventArgs(source)); - - // Also raise base class event in case this event has been subscribed - OnProcessingComplete(); - } - catch (Exception ex) - { - // We protect our code from consumer thrown exceptions - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ProcessingComplete event: {ex.Message}", ex), "ConsumerEventException"); - } - } - - /// - /// Raises the event. - /// - /// Message for the notification. - protected void OnNotificationReceived(string message) - { - try - { - NotificationReceived?.Invoke(this, new EventArgs(message)); - } - catch (Exception ex) - { - // We protect our code from consumer thrown exceptions - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for NotificationReceived event: {ex.Message}", ex), "ConsumerEventException"); - } - } - - /// - /// Raises the event. - /// - protected void OnServerConfigurationChanged() - { - try - { - ServerConfigurationChanged?.Invoke(this, EventArgs.Empty); - } - catch (Exception ex) - { - // We protect our code from consumer thrown exceptions - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ServerConfigurationChanged event: {ex.Message}", ex), "ConsumerEventException"); - } - } - - /// - /// Raises event. - /// - /// The to assign to this message - /// Processing . - /// A fixed string to classify this event; defaults to null. - /// to use, if any; defaults to . - protected override void OnProcessException(MessageLevel level, Exception exception, string eventName = null, MessageFlags flags = MessageFlags.None) - { - base.OnProcessException(level, exception, eventName, flags); - - // Just in case Log Message Suppression was turned on, turn it off so this code can raise messages - using (Logger.OverrideSuppressLogMessages()) - { - if (DateTime.UtcNow.Ticks - m_lastParsingExceptionTime > ParsingExceptionWindow) - { - // Exception window has passed since last exception, so we reset counters - m_lastParsingExceptionTime = DateTime.UtcNow.Ticks; - m_parsingExceptionCount = 0; - } - - m_parsingExceptionCount++; - - if (m_parsingExceptionCount <= AllowedParsingExceptions) - return; - - try - { - // When the parsing exception threshold has been exceeded, connection is restarted - Start(); - } - catch (Exception ex) - { - base.OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Error while restarting subscriber connection due to excessive exceptions: {ex.Message}", ex), "DataSubscriber", MessageFlags.UsageIssue); - } - finally - { - // Notify consumer of parsing exception threshold deviation - OnExceededParsingExceptionThreshold(); - m_lastParsingExceptionTime = 0; - m_parsingExceptionCount = 0; - } - } - } - - /// - /// Raises the event. - /// - private void OnExceededParsingExceptionThreshold() - { - ExceededParsingExceptionThreshold?.Invoke(this, EventArgs.Empty); - } - - // Updates the measurements per second counters after receiving another set of measurements. - private void UpdateMeasurementsPerSecond(DateTime now, int measurementCount) - { - long secondsSinceEpoch = now.Ticks / Ticks.PerSecond; - - if (secondsSinceEpoch > m_lastSecondsSinceEpoch) - { - if (m_measurementsInSecond < MinimumMeasurementsPerSecond || MinimumMeasurementsPerSecond == 0L) - MinimumMeasurementsPerSecond = m_measurementsInSecond; - - if (m_measurementsInSecond > MaximumMeasurementsPerSecond || MaximumMeasurementsPerSecond == 0L) - MaximumMeasurementsPerSecond = m_measurementsInSecond; - - m_totalMeasurementsPerSecond += m_measurementsInSecond; - m_measurementsPerSecondCount++; - m_measurementsInSecond = 0L; - - m_lastSecondsSinceEpoch = secondsSinceEpoch; - } - - m_measurementsInSecond += measurementCount; - } - - // Resets the measurements per second counters after reading the values from the last calculation interval. - private void ResetMeasurementsPerSecondCounters() - { - MinimumMeasurementsPerSecond = 0L; - MaximumMeasurementsPerSecond = 0L; - m_totalMeasurementsPerSecond = 0L; - m_measurementsPerSecondCount = 0L; - } - - private void UpdateStatisticsHelpers() - { - List> statisticsHelpers = m_statisticsHelpers; - - if (statisticsHelpers is null) - return; - - long now = RealTime; - - foreach (DeviceStatisticsHelper statisticsHelper in statisticsHelpers) - { - statisticsHelper?.Update(now); - - // FUTURE: Missing data detection could be complex. For example, no need to continue logging data outages for devices that are offline - but how to detect? - // If data channel is UDP, measurements are missing for time span and data gap recovery enabled, request missing - //if m_dataChannel is not null && m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null && m_lastMeasurementCheck > 0 && - // statisticsHelper.Device.MeasurementsExpected - statisticsHelper.Device.MeasurementsReceived > m_minimumMissingMeasurementThreshold) - // m_dataGapRecoverer.LogDataGap(m_lastMeasurementCheck - Ticks.FromSeconds(m_transmissionDelayTimeAdjustment), now); - } - - //m_lastMeasurementCheck = now; - } - - private void SubscribedDevicesTimer_Elapsed(object sender, EventArgs elapsedEventArgs) - { - UpdateStatisticsHelpers(); - } - - private bool SynchronizedMetadataChanged(DataSet newSynchronizedMetadata) - { - try - { - return !DataSetEqualityComparer.Default.Equals(m_synchronizedMetadata, newSynchronizedMetadata); - } - catch - { - return true; - } - } - - /// - /// Gets file path for any defined logging path. - /// - /// Path to acquire within logging path. - /// File path within any defined logging path. - protected string GetLoggingPath(string filePath) - { - return string.IsNullOrWhiteSpace(m_loggingPath) ? - FilePath.GetAbsolutePath(filePath) : - Path.Combine(m_loggingPath, filePath); - } - - private void DataStreamMonitor_Elapsed(object sender, EventArgs e) - { - bool dataReceived = m_monitoredBytesReceived > 0; - - if (m_dataChannel is null && m_metadataRefreshPending) - { - if (m_lastReceivedAt > DateTime.MinValue) - dataReceived = (DateTime.UtcNow - m_lastReceivedAt).Seconds < DataLossInterval; - } - - if (!dataReceived) - { - // If we've received no data in the last time-span, we restart connect cycle... - m_dataStreamMonitor.Enabled = false; - OnStatusMessage(MessageLevel.Info, $"{Environment.NewLine}No data received in {m_dataStreamMonitor.Interval / 1000.0D:0.0} seconds, restarting connect cycle...{Environment.NewLine}", "Connection Issues"); - - ThreadPool.QueueUserWorkItem(_ => - { - if (m_serverCommandChannel is null) - Restart(); - else - DisconnectClient(); - }); - } - - // Reset bytes received bytes being monitored - m_monitoredBytesReceived = 0L; - } - - private void RunTimeLog_ProcessException(object sender, EventArgs e) - { - OnProcessException(MessageLevel.Info, e.Argument); - } - - private void DataGapRecoverer_RecoveredMeasurements(object sender, EventArgs> e) - { - OnNewMeasurements(e.Argument); - } - - private void DataGapRecoverer_StatusMessage(object sender, EventArgs e) - { - OnStatusMessage(MessageLevel.Info, "[DataGapRecoverer] " + e.Argument); - } - - private void DataGapRecoverer_ProcessException(object sender, EventArgs e) - { - OnProcessException(MessageLevel.Warning, new InvalidOperationException("[DataGapRecoverer] " + e.Argument.Message, e.Argument.InnerException)); - } - - #region [ Client Command Channel Event Handlers ] - - private void ClientCommandChannelConnectionEstablished(object sender, EventArgs e) - { - // Define operational modes as soon as possible - SendServerCommand(ServerCommand.DefineOperationalModes, BigEndian.GetBytes((uint)m_operationalModes)); - - // Notify input adapter base that asynchronous connection succeeded - if (!PersistConnectionForMetadata) - OnConnected(); - else - SendServerCommand(ServerCommand.MetaDataRefresh, MetadataFilters); - - // Notify consumer that connection was successfully established - OnConnectionEstablished(); - - OnStatusMessage(MessageLevel.Info, m_serverCommandChannel is null ? - "Data subscriber command channel connection to publisher was established." : - "Data subscriber server-based command channel established a new client connection from the publisher."); - - if (AutoConnect && Enabled) - StartSubscription(); - - if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null) - m_dataGapRecoverer.Enabled = true; - } - - private void ClientCommandChannelConnectionTerminated(object sender, EventArgs e) - { - OnConnectionTerminated(); - - OnStatusMessage(MessageLevel.Info, m_serverCommandChannel is null ? - "Data subscriber command channel connection to publisher was terminated." : - "Data subscriber server-based command channel client connection from the publisher was terminated."); - - DisconnectClient(); - } - - private void ClientCommandChannelConnectionException(object sender, EventArgs e) - { - Exception ex = e.Argument; - OnProcessException(MessageLevel.Info, new ConnectionException($"Data subscriber encountered an exception while attempting command channel publisher connection: {ex.Message}", ex)); - } - - private void ClientCommandChannelConnectionAttempt(object sender, EventArgs e) - { - // Inject a short delay between multiple connection attempts - if (m_commandChannelConnectionAttempts > 0) - Thread.Sleep(2000); - - OnStatusMessage(MessageLevel.Info, "Attempting command channel connection to publisher..."); - m_commandChannelConnectionAttempts++; - } - - private void ClientCommandChannelSendDataException(object sender, EventArgs e) - { - Exception ex = e.Argument; - - if (!HandleSocketException(ex) && ex is not ObjectDisposedException) - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data subscriber encountered an exception while sending command channel data to publisher connection: {ex.Message}", ex)); - } - - private void ClientCommandChannelReceiveData(object sender, EventArgs e) - { - try - { - int length = e.Argument; - byte[] buffer = new byte[length]; - - m_lastBytesReceived = length; - m_lastReceivedAt = DateTime.UtcNow; - - m_clientCommandChannel?.Read(buffer, 0, length); - m_serverCommandChannel?.Read(m_activeClientID, buffer, 0, length); - - ProcessServerResponse(buffer, length); - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Info, ex); - } - } - - private void ClientCommandChannelReceiveDataException(object sender, EventArgs e) - { - Exception ex = e.Argument; - - if (!HandleSocketException(ex) && ex is not ObjectDisposedException) - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data subscriber encountered an exception while receiving command channel data from publisher connection: {ex.Message}", ex)); - } - - #endregion - - #region [ Server Command Channel Event Handlers ] - - private void ServerCommandChannelReceiveClientData(object sender, EventArgs e) - { - ClientCommandChannelReceiveData(sender, new EventArgs(e.Argument2)); - } - - private void ServerCommandChannelClientConnected(object sender, EventArgs e) - { - m_activeClientID = e.Argument; - - // Reset all connection stats when new publisher-client connects - this is equivalent - // to a normal client-based subscriber establishing a new connection to the publisher - List> statisticsHelpers = m_statisticsHelpers; - - if (statisticsHelpers is not null) - { - long now = UseLocalClockAsRealTime ? DateTime.UtcNow.Ticks : 0L; - m_realTime = 0L; - m_lastStatisticsHelperUpdate = 0L; - - foreach (DeviceStatisticsHelper statisticsHelper in statisticsHelpers) - statisticsHelper.Reset(now); - } - - if (UseLocalClockAsRealTime) - m_subscribedDevicesTimer.Start(); - - ClientCommandChannelConnectionEstablished(sender, EventArgs.Empty); - } - - private void ServerCommandChannelClientDisconnected(object sender, EventArgs e) - { - ClientCommandChannelConnectionTerminated(sender, EventArgs.Empty); - } - - private void ServerCommandChannelClientConnectingException(object sender, EventArgs e) - { - Exception ex = e.Argument; - OnProcessException(MessageLevel.Info, new ConnectionException($"Data subscriber encountered an exception while connecting client-based publisher to the command channel: {ex.Message}", ex)); - } - - private void ServerCommandChannelServerStarted(object sender, EventArgs e) - { - OnStatusMessage(MessageLevel.Info, "Data subscriber server-based command channel started."); - } - - private void ServerCommandChannelServerStopped(object sender, EventArgs e) - { - if (Enabled) - { - OnStatusMessage(MessageLevel.Info, "Data subscriber server-based command channel was unexpectedly terminated, restarting..."); - - Action restartServerCommandChannel = () => - { - try - { - m_serverCommandChannel.Start(); - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to restart data subscriber server-based command channel: {ex.Message}", ex)); - } - }; - - // We must wait for command channel to completely shutdown before trying to restart... - restartServerCommandChannel.DelayAndExecute(2000); - } - else - { - OnStatusMessage(MessageLevel.Info, "Data subscriber server-based command channel stopped."); - } - } - - private void ServerCommandChannelSendClientDataException(object sender, EventArgs e) - { - Exception ex = e.Argument2; - - if (HandleSocketException(ex)) - return; - - if (ex is not NullReferenceException && ex is not ObjectDisposedException) - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data subscriber encountered an exception while sending command channel data to client-based publisher connection: {ex.Message}", ex)); - - DisconnectClient(); - } - - private void ServerCommandChannelReceiveClientDataException(object sender, EventArgs e) - { - Exception ex = e.Argument2; - - if (HandleSocketException(ex)) - return; - - if (ex is not NullReferenceException && ex is not ObjectDisposedException) - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data subscriber encountered an exception while receiving command channel data from client-based publisher connection: {ex.Message}", ex)); - - DisconnectClient(); - } - - #endregion - - #region [ Data Channel Event Handlers ] - - private void DataChannelConnectionException(object sender, EventArgs e) - { - Exception ex = e.Argument; - OnProcessException(MessageLevel.Info, new ConnectionException($"Data subscriber encountered an exception while attempting to establish UDP data channel connection: {ex.Message}", ex)); - } - - private void DataChannelConnectionAttempt(object sender, EventArgs e) - { - // Inject a short delay between multiple connection attempts - if (m_dataChannelConnectionAttempts > 0) - Thread.Sleep(2000); - - OnStatusMessage(MessageLevel.Info, "Attempting to establish data channel connection to publisher..."); - m_dataChannelConnectionAttempts++; - } - - private void DataChannelReceiveData(object sender, EventArgs e) - { - try - { - int length = e.Argument; - byte[] buffer = new byte[length]; - - m_lastBytesReceived = length; - - m_dataChannel.Read(buffer, 0, length); - ProcessServerResponse(buffer, length); - } - catch (Exception ex) - { - OnProcessException(MessageLevel.Info, ex); - } - } - - private void DataChannelReceiveDataException(object sender, EventArgs e) - { - Exception ex = e.Argument; - - if (!HandleSocketException(ex) && ex is not ObjectDisposedException) - OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data subscriber encountered an exception while receiving UDP data from publisher connection: {ex.Message}", ex)); - } - - #endregion - - #endregion - - #region [ Static ] - - // Static Methods - - /// - /// Gets the path to the local certificate from the configuration file. - /// - /// Path to the local certificate from the configuration file. - internal static string GetLocalCertificate() - { - CategorizedSettingsElement localCertificateElement = ConfigurationFile.Current.Settings["systemSettings"]["LocalCertificate"]; - string localCertificate = null; - - if (localCertificateElement is not null) - localCertificate = localCertificateElement.Value; - - if (localCertificate is null || !File.Exists(FilePath.GetAbsolutePath(localCertificate))) - throw new InvalidOperationException("Unable to find local certificate. Local certificate file must exist when using TLS security mode."); - - return localCertificate; - } - - /// - /// Checks if the specified certificate exists, updating path if needed. - /// - /// Reference certificate. - /// true if certificate exists; otherwise, false. - internal static bool RemoteCertificateExists(ref string remoteCertificate) - { - if (File.Exists(FilePath.GetAbsolutePath(remoteCertificate))) - return true; - - CategorizedSettingsElement remoteCertificateElement = ConfigurationFile.Current.Settings["systemSettings"]["RemoteCertificatesPath"]; - - if (remoteCertificateElement is null) - return false; - - remoteCertificate = Path.Combine(remoteCertificateElement.Value, remoteCertificate); - - return File.Exists(FilePath.GetAbsolutePath(remoteCertificate)); - } - - #endregion +//****************************************************************************************************** +// DataSubscriber.cs - Gbtc +// +// Copyright © 2012, 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://www.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/20/2010 - J. Ritchie Carroll +// Generated original version of source code. +// 02/07/2012 - Mehulbhai Thakkar +// Modified SynchronizeMetadata to filter devices by original source and modified insert query +// to populate OriginalSource value. Added to flag to optionally avoid meta-data synchronization. +// 12/20/2012 - Starlynn Danyelle Gilliam +// Modified Header. +// +//****************************************************************************************************** +// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident +// ReSharper disable BadControlBracesIndent +// ReSharper disable UnusedVariable + +#pragma warning disable SYSLIB0057 + +using sttp.tssc; + +namespace sttp; + +/// +/// Represents a data subscribing client that will connect to a data publisher for a data subscription. +/// +[Description("STTP Subscriber: client that subscribes to an STTP-style publishing server for a streaming data.")] +[EditorBrowsable(EditorBrowsableState.Advanced)] // Normally defined as an input device protocol +#if NET +[AdapterProtocol("STTP", "Streaming Telemetry Transport Protocol", ProtocolType.Measurement, UIVisibility.Input, false)] +[UIAdapterProtocol("STTP", "sttp.gemstone", "sttp.UI.STTP.js")] +[UIAdapterProtocol("STTP", "sttp.gemstone", "sttp.UI.STTPChunk.js")] +#endif +public class DataSubscriber : InputAdapterBase +{ + #region [ Members ] + + // Nested Types + + private class SubscribedDevice : IDevice, IDisposable + { + #region [ Members ] + + // Fields + private long m_dataQualityErrors; + private long m_timeQualityErrors; + private long m_deviceErrors; + private long m_measurementsReceived; + private double m_measurementsExpected; + private long m_measurementsWithError; + private long m_measurementsDefined; + private bool m_disposed; + + #endregion + + #region [ Constructors ] + + public SubscribedDevice(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + StatisticsEngine.Register(this, name, "Device", "PMU"); + } + + /// + /// Releases the unmanaged resources before the object is reclaimed by . + /// + ~SubscribedDevice() + { + Unregister(); + } + + #endregion + + #region [ Properties ] + + public string Name { get; } + + public Guid StatusFlagsID { get; set; } + + public Guid FrequencyID { get; set; } + + public Guid DeltaFrequencyID { get; set; } + + public long DataQualityErrors + { + get => Interlocked.Read(ref m_dataQualityErrors); + set => Interlocked.Exchange(ref m_dataQualityErrors, value); + } + + public long TimeQualityErrors + { + get => Interlocked.Read(ref m_timeQualityErrors); + set => Interlocked.Exchange(ref m_timeQualityErrors, value); + } + + public long DeviceErrors + { + get => Interlocked.Read(ref m_deviceErrors); + set => Interlocked.Exchange(ref m_deviceErrors, value); + } + + public long MeasurementsReceived + { + get => Interlocked.Read(ref m_measurementsReceived); + set => Interlocked.Exchange(ref m_measurementsReceived, value); + } + + public long MeasurementsExpected + { + get => (long)Interlocked.CompareExchange(ref m_measurementsExpected, 0.0D, 0.0D); + set => Interlocked.Exchange(ref m_measurementsExpected, value); + } + + public long MeasurementsWithError + { + get => Interlocked.Read(ref m_measurementsWithError); + set => Interlocked.Exchange(ref m_measurementsWithError, value); + } + + public long MeasurementsDefined + { + get => Interlocked.Read(ref m_measurementsDefined); + set => Interlocked.Exchange(ref m_measurementsDefined, value); + } + + #endregion + + #region [ Methods ] + + public override bool Equals(object? obj) + { + return obj is SubscribedDevice subscribedDevice && Name.Equals(subscribedDevice.Name); + } + + public override int GetHashCode() + { + return Name.GetHashCode(); + } + + /// + /// Releases all the resources used by the object. + /// + public void Dispose() + { + Unregister(); + GC.SuppressFinalize(this); + } + + private void Unregister() + { + if (m_disposed) + return; + + try + { + StatisticsEngine.Unregister(this); + } + finally + { + m_disposed = true; // Prevent duplicate dispose. + } + } + + #endregion + } + + /// + /// EventArgs implementation for handling user commands. + /// + public class UserCommandArgs : EventArgs + { + /// + /// Creates a new instance of the class. + /// + /// The code for the user command. + /// The code for the server's response. + /// Buffer containing the message from the server. + /// Index into the buffer used to skip the header. + /// The length of the message in the buffer, including the header. + public UserCommandArgs(ServerCommand command, ServerResponse response, byte[] buffer, int startIndex, int length) + { + Command = command; + Response = response; + Buffer = buffer; + StartIndex = startIndex; + Length = length; + } + + /// + /// Gets the code for the user command. + /// + public ServerCommand Command { get; } + + /// + /// Gets the code for the server's response. + /// + public ServerResponse Response { get; } + + /// + /// Gets the buffer containing the message from the server. + /// + public byte[] Buffer { get; } + + /// + /// Gets the index into the buffer used to skip the header. + /// + public int StartIndex { get; } + + /// + /// Gets the length of the message in the buffer, including the header. + /// + public int Length { get; } + } + + // Constants + + /// + /// Defines default value for property. + /// + public const OperationalModes DefaultOperationalModes = (OperationalModes)((uint)OperationalModes.VersionMask & 2U) | OperationalModes.CompressMetadata | OperationalModes.CompressSignalIndexCache | OperationalModes.ReceiveInternalMetadata; + + /// + /// Defines the default value for the property. + /// + public const int DefaultMetadataSynchronizationTimeout = 0; + + /// + /// Defines the default value for the property. + /// + public const bool DefaultUseTransactionForMetadata = true; + + /// + /// Default value for property. + /// + public const string DefaultLoggingPath = "ConfigurationCache"; + + /// + /// Specifies the default value for the property. + /// + public const int DefaultAllowedParsingExceptions = 10; + + /// + /// Specifies the default value for the property. + /// + public const long DefaultParsingExceptionWindow = 50000000L; // 5 seconds + + private const int EvenKey = 0; // Even key/IV index + private const int OddKey = 1; // Odd key/IV index + private const int KeyIndex = 0; // Index of cipher key component in keyIV array + private const int IVIndex = 1; // Index of initialization vector component in keyIV array + + private const long MissingCacheWarningInterval = 20000000; + + // Events + + /// + /// Occurs when client connection to the data publication server is established. + /// + public event EventHandler? ConnectionEstablished; + + /// + /// Occurs when client connection to the data publication server is terminated. + /// + public event EventHandler? ConnectionTerminated; + + /// + /// Occurs when client connection to the data publication server has successfully authenticated. + /// + public event EventHandler? ConnectionAuthenticated; + + /// + /// Occurs when client receives response from the server. + /// + public event EventHandler>? ReceivedServerResponse; + + /// + /// Occurs when client receives message from the server in response to a user command. + /// + public event EventHandler? ReceivedUserCommandResponse; + + /// + /// Occurs when client receives requested meta-data transmitted by data publication server. + /// + public event EventHandler>? MetaDataReceived; + + /// + /// Occurs when first measurement is transmitted by data publication server. + /// + public event EventHandler>? DataStartTime; + + /// + /// Indicates that processing for an input adapter (via temporal session) has completed. + /// + /// + /// This event is expected to only be raised when an input adapter has been designed to process + /// a finite amount of data, e.g., reading a historical range of data during temporal processing. + /// + public new event EventHandler>? ProcessingComplete; + + /// + /// Occurs when a notification has been received from the . + /// + public event EventHandler>? NotificationReceived; + + /// + /// Occurs when the server has sent a notification that its configuration has changed, this + /// can allow subscriber to request updated meta-data if desired. + /// + public event EventHandler? ServerConfigurationChanged; + + /// + /// Occurs when number of parsing exceptions exceed during . + /// + public event EventHandler? ExceededParsingExceptionThreshold; + + // Fields + private IClient? m_clientCommandChannel; + private IServer? m_serverCommandChannel; + private UdpClient? m_dataChannel; + private Guid m_activeClientID; + private string? m_connectionID; + private SharedTimer? m_dataStreamMonitor; + private long m_commandChannelConnectionAttempts; + private long m_dataChannelConnectionAttempts; + private volatile SignalIndexCache? m_remoteSignalIndexCache; + private volatile SignalIndexCache?[]? m_signalIndexCache; +#if NET + private readonly Lock m_signalIndexCacheLock; +#else + private readonly object m_signalIndexCacheLock; +#endif + private volatile int m_cacheIndex; + private volatile long[]? m_baseTimeOffsets; + private volatile int m_timeIndex; + private volatile byte[][][]? m_keyIVs; + private volatile bool m_subscribed; + private volatile int m_lastBytesReceived; + private DateTime m_lastReceivedAt; + private long m_monitoredBytesReceived; + private long m_lastMissingCacheWarning; +#if !NET + private Guid m_nodeID; + private int m_sttpProtocolID; +#endif + private bool m_includeTime; + private bool m_metadataRefreshPending; + private readonly LongSynchronizedOperation m_synchronizeMetadataOperation; + private volatile DataSet? m_receivedMetadata; + private DataSet? m_synchronizedMetadata; + private DateTime m_lastMetaDataRefreshTime; + private OperationalModes m_operationalModes; + private string? m_loggingPath; + private RunTimeLog? m_runTimeLog; + private bool m_dataGapRecoveryEnabled; + private DataGapRecoverer? m_dataGapRecoverer; + private int m_parsingExceptionCount; + private long m_lastParsingExceptionTime; + + private bool m_supportsTemporalProcessing; + private volatile Dictionary>? m_subscribedDevicesLookup; + private volatile List>? m_statisticsHelpers; + private readonly LongSynchronizedOperation m_registerStatisticsOperation; + + private readonly List m_bufferBlockCache; + private uint m_expectedBufferBlockSequenceNumber; + + private Ticks m_realTime; + private Ticks m_lastStatisticsHelperUpdate; + private SharedTimer? m_subscribedDevicesTimer; + + private long m_totalMeasurementsPerSecond; + private long m_measurementsPerSecondCount; + private long m_measurementsInSecond; + private long m_lastSecondsSinceEpoch; + private long m_lifetimeTotalLatency; + private long m_lifetimeMinimumLatency; + private long m_lifetimeMaximumLatency; + private long m_lifetimeLatencyMeasurements; + + private long m_syncProgressTotalActions; + private long m_syncProgressActionsCount; + private long m_syncProgressLastMessage; + + private bool m_disposed; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new . + /// + public DataSubscriber() + { + m_registerStatisticsOperation = new LongSynchronizedOperation(HandleDeviceStatisticsRegistration) + { + IsBackground = true + }; + + m_synchronizeMetadataOperation = new LongSynchronizedOperation(SynchronizeMetadata) + { + IsBackground = true + }; + + Encoding = Encoding.Unicode; + m_operationalModes = DefaultOperationalModes; + MetadataSynchronizationTimeout = DefaultMetadataSynchronizationTimeout; + AllowedParsingExceptions = DefaultAllowedParsingExceptions; + ParsingExceptionWindow = DefaultParsingExceptionWindow; + + string loggingPath = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(DefaultLoggingPath)); + + if (Directory.Exists(loggingPath)) + m_loggingPath = loggingPath; + + // Default to not using transactions for meta-data on SQL server (helps avoid deadlocks) + try + { + #if NET + using AdoDataConnection database = new(ConfigSettings.Instance); + #else + using AdoDataConnection database = new("systemSettings"); + #endif + UseTransactionForMetadata = database.DatabaseType != DatabaseType.SQLServer; + } + catch + { + UseTransactionForMetadata = DefaultUseTransactionForMetadata; + } + + DataLossInterval = 10.0D; + m_bufferBlockCache = []; + UseLocalClockAsRealTime = true; + UseSourcePrefixNames = true; + m_signalIndexCacheLock = new(); + } + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets the security mode used for communications over the command channel. + /// + public SecurityMode SecurityMode { get; set; } + + /// + /// Gets or sets logging path to be used to be runtime and outage logs of the subscriber which are required for + /// automated data recovery. + /// + /// + /// Leave value blank for default path, i.e., installation folder. Can be a fully qualified path or a path that + /// is relative to the installation folder, e.g., a value of "ConfigurationCache" might resolve to + /// "C:\Program Files\MyTimeSeriesApp\ConfigurationCache\". + /// + public string LoggingPath + { + get => m_loggingPath ?? ""; + set + { + if (!string.IsNullOrWhiteSpace(value)) + { + string loggingPath = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(value)); + + if (Directory.Exists(loggingPath)) + value = loggingPath; + } + + m_loggingPath = value; + } + } + + /// + /// Gets or sets flag that determines if should attempt to auto-connection to using defined connection settings. + /// + public bool AutoConnect { get; set; } + + /// + /// Gets or sets flag that determines if should + /// automatically request meta-data synchronization and synchronize publisher + /// meta-data with its own database configuration. + /// + public bool AutoSynchronizeMetadata { get; set; } + + /// + /// Gets flag that indicates whether the connection will be persisted + /// even while the adapter is offline in order to synchronize metadata. + /// + public bool PersistConnectionForMetadata => + !AutoStart && AutoSynchronizeMetadata && !this.TemporalConstraintIsDefined(); + + /// + /// Gets or sets flag that determines if child devices associated with a subscription + /// should be prefixed with the subscription name and an exclamation point to ensure + /// device name uniqueness - recommended value is true. + /// + public bool UseSourcePrefixNames { get; set; } + + /// + /// Gets or sets requested meta-data filter expressions to be applied by before meta-data is sent. + /// + /// + /// Multiple meta-data filters, such filters for different data tables, should be separated by a semicolon. Specifying fields in the filter + /// expression that do not exist in the data publisher's current meta-data set could cause filter expressions to not be applied and possibly + /// result in no meta-data being received for the specified data table. + /// + /// + /// FILTER MeasurementDetail WHERE SignalType <> 'STAT'; FILTER PhasorDetail WHERE Phase = '+' + /// + public string? MetadataFilters { get; set; } + + /// + /// Gets or sets flag that determines if a subscription is mutual, i.e., bidirectional pub/sub. In this mode one node will + /// be the owner and set Internal = True and the other node will be the renter and set Internal = False. + /// + /// + /// + /// This flag is intended to be used in scenarios where a remote subscriber can add new measurements associated with a + /// source device, e.g., creating new calculated result measurements on a remote machine for load distribution that should + /// get associated with a device on the local machine, thus becoming part of the local measurement set. + /// + /// + /// For best results, both the owner and renter subscriptions should be reduced to needed measurements, i.e., renter should + /// only receive measurements needed for remote calculations and owner should only receive new calculated results. Note that + /// when used with a TLS-style subscription this can be accomplished by using the subscription UI screens that control the + /// measurement subscribed flag. For internal subscriptions, reduction of metadata and subscribed measurements will + /// need to be controlled via connection string with metadataFilters and outputMeasurements, respectively. + /// + /// + /// Setting to true will force to true, + /// to false, and to false. + /// + /// + public bool MutualSubscription { get; set; } + + /// + /// Gets or sets flag that informs publisher if base time-offsets can use millisecond resolution to conserve bandwidth. + /// + [Obsolete("SubscriptionInfo object defines this parameter.", false)] + public bool UseMillisecondResolution { get; set; } + +#if !NET + /// + /// Gets or sets flag that determines if a should be used for command channel connections. + /// + public bool UseSimpleTcpClient { get; set; } +#endif + + /// + /// Gets flag that determines whether the command channel is connected. + /// + public bool CommandChannelConnected => m_clientCommandChannel?.Enabled ?? m_serverCommandChannel?.Enabled ?? false; + + /// + /// Gets total data packet bytes received during this session. + /// + public long TotalBytesReceived { get; private set; } + + /// + /// Gets or sets data loss monitoring interval, in seconds. Set to zero to disable monitoring. + /// + public double DataLossInterval + { + get => m_dataStreamMonitor?.Interval / 1000.0D ?? 0.0D; + set + { + if (value > 0.0D) + { + if (m_dataStreamMonitor is null) + { + // Create data stream monitoring timer + m_dataStreamMonitor = Common.TimerScheduler.CreateTimer(); + m_dataStreamMonitor!.Elapsed += DataStreamMonitor_Elapsed; + m_dataStreamMonitor.AutoReset = true; + m_dataStreamMonitor.Enabled = false; + } + + // Set user specified interval + m_dataStreamMonitor.Interval = (int)(value * 1000.0D); + } + else + { + // Disable data monitor + if (m_dataStreamMonitor is not null) + { + m_dataStreamMonitor.Elapsed -= DataStreamMonitor_Elapsed; + m_dataStreamMonitor.Dispose(); + } + + m_dataStreamMonitor = null; + } + } + } + + /// + /// Gets or sets a set of flags that define ways in which the subscriber and publisher communicate. + /// + public OperationalModes OperationalModes + { + get => m_operationalModes; + set + { + m_operationalModes = value; + OperationalEncoding operationalEncoding = (OperationalEncoding)(value & OperationalModes.EncodingMask); + Encoding = GetCharacterEncoding(operationalEncoding); + } + } + + /// + /// Gets or sets the operational mode flag to compress meta-data. + /// + public bool CompressMetadata + { + get => m_operationalModes.HasFlag(OperationalModes.CompressMetadata); + set + { + if (value) + m_operationalModes |= OperationalModes.CompressMetadata; + else + m_operationalModes &= ~OperationalModes.CompressMetadata; + } + } + + /// + /// Gets or sets the operational mode flag to compress the signal index cache. + /// + public bool CompressSignalIndexCache + { + get => m_operationalModes.HasFlag(OperationalModes.CompressSignalIndexCache); + set + { + if (value) + m_operationalModes |= OperationalModes.CompressSignalIndexCache; + else + m_operationalModes &= ~OperationalModes.CompressSignalIndexCache; + } + } + + /// + /// Gets or sets the operational mode flag to compress data payloads. + /// + public bool CompressPayload + { + get => m_operationalModes.HasFlag(OperationalModes.CompressPayloadData); + set + { + if (value) + m_operationalModes |= OperationalModes.CompressPayloadData; + else + m_operationalModes &= ~OperationalModes.CompressPayloadData; + } + } + + /// + /// Gets or sets the operational mode flag to receive internal meta-data. + /// + public bool ReceiveInternalMetadata + { + get => m_operationalModes.HasFlag(OperationalModes.ReceiveInternalMetadata); + set + { + if (value) + m_operationalModes |= OperationalModes.ReceiveInternalMetadata; + else + m_operationalModes &= ~OperationalModes.ReceiveInternalMetadata; + } + } + + /// + /// Gets or sets flag that determines if measurements for this data subscription should be marked as "internal", i.e., owned and allowed for proxy. + /// + public bool Internal { get; set; } + + /// + /// Gets or sets flag that determines if output measurements should be automatically filtered to only those belonging to the subscriber. + /// + public bool FilterOutputMeasurements { get; set; } + + /// + /// Gets or sets flag that determines if identity inserts should be used for SQL Server connections during meta-data synchronization. + /// + public bool UseIdentityInsertsForMetadata { get; set; } + + /// + /// Gets or sets flag that determines if CALC signals not defined in metadata should be deleted during synchronizations. Do not set this + /// value to true if local calculations are being created, and associated with, data arriving from STTP stream. + /// + public bool AutoDeleteCalculatedMeasurements { get; set; } + + /// + /// Gets or sets flag that determines if ALARM signals not defined in metadata should be deleted during synchronizations. Do not set this + /// value to true if local alarms are being created, and associated with, data arriving from STTP stream. + /// + public bool AutoDeleteAlarmMeasurements { get; set; } + + /// + /// Gets or sets flag that determines if the data subscriber should attempt to synchronize device metadata as independent devices, i.e., + /// not as children of the parent STTP device connection. + /// Defaults to false. + /// + /// + /// This is useful when using an STTP connection to only synchronize metadata from a publisher, but not to receive data. When enabled, + /// the device enabled state will not be synchronized upon creation unless is set to + /// true. In this mode it may be useful to add the original "ConnectionString" field to the publisher's device metadata so it can + /// be synchronized to the subscriber. To ensure no data is received, the subscriber should be configured with an "OutputMeasurements" + /// filter in the adapter's connection string that does not include any measurements, e.g.: + /// outputMeasurements={FILTER ActiveMeasurements WHERE False} + /// + public bool SyncIndependentDevices { get; set; } + + /// + /// Gets or sets flag that determines if the data subscriber should automatically enable independently synced devices. + /// Defaults to false. + /// + public bool AutoEnableIndependentlySyncedDevices { get; set; } + + /// + /// Gets or sets flag that determines if statistics engine should be enabled for the data subscriber. + /// + public bool BypassStatistics { get; set; } + + /// + /// Gets or sets the operational mode flag to receive external meta-data. + /// + public bool ReceiveExternalMetadata + { + get => m_operationalModes.HasFlag(OperationalModes.ReceiveExternalMetadata); + set + { + if (value) + m_operationalModes |= OperationalModes.ReceiveExternalMetadata; + else + m_operationalModes &= ~OperationalModes.ReceiveExternalMetadata; + } + } + + /// + /// Gets or sets the used by the subscriber and publisher. + /// + public OperationalEncoding OperationalEncoding + { + get => (OperationalEncoding)(m_operationalModes & OperationalModes.EncodingMask); + set + { + m_operationalModes &= ~OperationalModes.EncodingMask; + m_operationalModes |= (OperationalModes)value; + Encoding = GetCharacterEncoding(value); + } + } + + /// + /// Gets or sets the used by the subscriber and publisher. + /// + public CompressionModes CompressionModes + { + get => (CompressionModes)(m_operationalModes & OperationalModes.CompressionModeMask); + set + { + m_operationalModes &= ~OperationalModes.CompressionModeMask; + m_operationalModes |= (OperationalModes)value; + + if (value.HasFlag(CompressionModes.TSSC)) + CompressPayload = true; + } + } + + /// + /// Gets or sets the version number of the protocol in use by this subscriber. + /// + public int Version + { + get => (int)(m_operationalModes & OperationalModes.VersionMask); + set + { + m_operationalModes &= ~OperationalModes.VersionMask; + m_operationalModes |= (OperationalModes)value; + } + } + + /// + /// Gets the character encoding defined by the + /// of the communications stream. + /// + public Encoding Encoding { get; private set; } + + /// + /// Gets flag indicating if this adapter supports real-time processing. + /// + /// + /// Setting this value to false indicates that the adapter should not be enabled unless it exists within a temporal session. + /// As an example, this flag can be used in a gateway system to set up two separate subscribers: one to the PDC for real-time + /// data streams and one to the historian for historical data streams. In this scenario, the assumption is that the PDC is + /// the data source for the historian, implying that only local data is destined for archival. + /// + public bool SupportsRealTimeProcessing { get; private set; } + + /// + /// Gets the flag indicating if this adapter supports temporal processing. + /// + /// + /// + /// Although the data subscriber provisions support for temporal processing by receiving historical data from a remote source, + /// the adapter opens sockets and does not need to be engaged within an actual temporal , therefore + /// this method normally returns false to make sure the adapter doesn't get instantiated within a temporal session. + /// + /// + /// Setting this to true means that a subscriber will be initialized within a temporal session to provide historical + /// data from a remote source - this should only be enabled in cases where (1) there is no locally defined, e.g., in-process, + /// historian that can already provide historical data for temporal sessions, and (2) a remote subscriber should be allowed + /// to proxy temporal requests, e.g., those requested for data gap recovery, to an up-stream subscription. This is useful in + /// cases where a primary data subscriber that has data gap recovery enabled can also allow a remote subscription to proxy in + /// data gap recovery requests. It is recommended that remote data gap recovery request parameters be (1) either slightly + /// looser than those of local system to reduce the possibility of duplicated recovery sessions for the same data loss, or + /// (2) only enabled in the end-most system that most needs the recovered data, like a historian. + /// + /// + public override bool SupportsTemporalProcessing => m_supportsTemporalProcessing; + + /// + /// Gets or sets the desired processing interval, in milliseconds, for the adapter. + /// + /// + /// Except for the values of -1 and 0, this value specifies the desired processing interval for data, i.e., basically a delay, + /// or timer interval, over which to process data. A value of -1 means to use the default processing interval while a value of + /// 0 means to process data as fast as possible. + /// + public override int ProcessingInterval + { + get => base.ProcessingInterval; + set + { + base.ProcessingInterval = value; + + // Request server update the processing interval + SendServerCommand(ServerCommand.UpdateProcessingInterval, BigEndian.GetBytes(value)); + } + } + + /// + /// Gets or sets the flag that determines whether to request that the subscription be throttled to certain publication interval, see . + /// + public bool Throttled { get; set; } + + /// + /// Gets or sets the interval, in seconds, at which data should be published when using a throttled subscription. + /// + public double PublishInterval { get; set; } = -1.0D; + + /// + /// Gets or sets the timeout used when executing database queries during meta-data synchronization. + /// + public int MetadataSynchronizationTimeout { get; set; } + + /// + /// Gets or sets flag that determines if meta-data synchronization should be performed within a transaction. + /// + public bool UseTransactionForMetadata { get; set; } + + /// + /// Gets or sets flag that determines whether to use the local clock when calculating statistics. + /// + public bool UseLocalClockAsRealTime { get; set; } + + /// + /// Gets or sets number of parsing exceptions allowed during before connection is reset. + /// + public int AllowedParsingExceptions { get; set; } + + /// + /// Gets or sets time duration, in , to monitor parsing exceptions. + /// + public Ticks ParsingExceptionWindow { get; set; } + + /// + /// Gets or sets based data source available to this . + /// + public override DataSet? DataSource + { + get => base.DataSource; + set + { + base.DataSource = value; + #if NET + m_registerStatisticsOperation.RunAsync(); + #else + m_registerStatisticsOperation.RunOnce(); + #endif + + bool outputMeasurementsUpdated = AutoConnect && UpdateOutputMeasurements(); + + // For automatic connections, when meta-data refresh is complete, update output measurements to see if any + // points for subscription have changed after re-application of filter expressions and if so, resubscribe + if (outputMeasurementsUpdated && Enabled && CommandChannelConnected) + { + OnStatusMessage(MessageLevel.Info, "Meta-data received from publisher modified measurement availability, adjusting active subscription..."); + + // Updating subscription will restart data stream monitor upon successful resubscribe + if (AutoStart) + SubscribeToOutputMeasurements(true); + } + + if (m_dataGapRecoverer is not null) + m_dataGapRecoverer.DataSource = value; + } + } + + /// + /// Gets or sets output measurement keys that are requested by other adapters based on what adapter says it can provide. + /// + public override MeasurementKey[]? RequestedOutputMeasurementKeys + { + get => base.RequestedOutputMeasurementKeys; + set + { + MeasurementKey[] oldKeys = base.RequestedOutputMeasurementKeys ?? []; + MeasurementKey[] newKeys = value ?? []; + HashSet oldKeySet = [.. oldKeys]; + + base.RequestedOutputMeasurementKeys = value; + + if (AutoStart || !Enabled || !CommandChannelConnected || oldKeySet.SetEquals(newKeys)) + return; + + OnStatusMessage(MessageLevel.Info, "Requested measurements have changed, adjusting active subscription..."); + SubscribeToOutputMeasurements(true); + } + } + + /// + /// Gets or sets output measurements that the will produce, if any. + /// + public override IMeasurement[]? OutputMeasurements + { + get => base.OutputMeasurements; + + set + { + base.OutputMeasurements = value; + + if (m_dataGapRecoverer is not null) + m_dataGapRecoverer.FilterExpression = this.OutputMeasurementKeys().Select(key => key.SignalID.ToString()).ToDelimitedString(';'); + } + } + + /// + /// Gets connection info for adapter, if any. + /// + public override string? ConnectionInfo + { + get + { + if (m_serverCommandChannel is not null && string.IsNullOrWhiteSpace(m_connectionID)) + { + Guid clientID = m_serverCommandChannel.ClientIDs.FirstOrDefault(); + IPEndPoint? endPoint = GetCommandChannelSocket()?.RemoteEndPoint as IPEndPoint; + m_connectionID = SubscriberConnection.GetEndPointConnectionID(clientID, endPoint); + } + + string? commandChannelServerUri = m_clientCommandChannel?.ServerUri ?? m_connectionID; + string? dataChannelServerUri = m_dataChannel?.ServerUri; + + if (string.IsNullOrWhiteSpace(commandChannelServerUri) && string.IsNullOrWhiteSpace(dataChannelServerUri)) + return null; + + if (string.IsNullOrWhiteSpace(dataChannelServerUri)) + return commandChannelServerUri; + + return string.IsNullOrWhiteSpace(commandChannelServerUri) ? dataChannelServerUri : $"{commandChannelServerUri} / {dataChannelServerUri}"; + } + } + + /// + /// Gets the status of this . + /// + /// + /// Derived classes should provide current status information about the adapter for display purposes. + /// + public override string Status + { + get + { + StringBuilder status = new(); + + status.AppendLine($" Protocol version: {Version}"); + status.AppendLine($" Connected: {CommandChannelConnected}"); + status.AppendLine($" Subscribed: {m_subscribed}"); + status.AppendLine($" Security mode: {SecurityMode}"); + status.AppendLine($" Authenticated: {SecurityMode == SecurityMode.TLS && CommandChannelConnected}"); + status.AppendLine($" Compression modes: {CompressionModes}"); + status.AppendLine($" Mutual subscription: {MutualSubscription}{(MutualSubscription ? $" - System has {(Internal ? "Owner" : "Renter")} Role" : "")}"); + status.AppendLine($" Mark received as internal: {Internal}"); + status.AppendLine($" Receive internal metadata: {ReceiveInternalMetadata}"); + status.AppendLine($" Receive external metadata: {ReceiveExternalMetadata}"); + status.AppendLine($"Filter output measurements: {FilterOutputMeasurements}"); + status.AppendLine($" Synchronize metadata IDs: {UseIdentityInsertsForMetadata}"); + status.AppendLine($" Auto delete CALC signals: {AutoDeleteCalculatedMeasurements}"); + status.AppendLine($" Auto delete ALRM signals: {AutoDeleteAlarmMeasurements}"); + status.AppendLine($" Sync independent devices: {SyncIndependentDevices}"); + + if (SyncIndependentDevices) + status.AppendLine($"Independent synced devices: {(AutoEnableIndependentlySyncedDevices ? "enabled" : "disabled")} on creation"); + + status.AppendLine($" Bypass statistics engine: {BypassStatistics}"); + status.AppendLine($" Total bytes received: {TotalBytesReceived:N0}"); + status.AppendLine($" Data packet security: {(SecurityMode == SecurityMode.TLS && m_dataChannel is null ? "Secured via TLS" : m_keyIVs is null ? "Unencrypted" : "AES Encrypted")}"); + status.AppendLine($" Data monitor enabled: {m_dataStreamMonitor is not null && m_dataStreamMonitor.Enabled}"); + status.AppendLine($" Logging path: {FilePath.TrimFileName(m_loggingPath.ToNonNullNorWhiteSpace(FilePath.GetAbsolutePath("")), 51)}"); + status.AppendLine($"No data reconnect interval: {(DataLossInterval > 0.0D ? $"{DataLossInterval:0.000} seconds" : "Disabled")}"); + status.AppendLine($" Data gap recovery mode: {(m_dataGapRecoveryEnabled ? "Enabled" : "Disabled")}"); + + if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null) + status.Append(m_dataGapRecoverer.Status); + + if (m_runTimeLog is not null) + { + status.AppendLine(); + status.AppendLine("Run-Time Log Status".CenterText(50)); + status.AppendLine("-------------------".CenterText(50)); + status.Append(m_runTimeLog.Status); + } + + if (m_dataChannel is not null) + { + status.AppendLine(); + status.AppendLine("Data Channel Status".CenterText(50)); + status.AppendLine("-------------------".CenterText(50)); + status.Append(m_dataChannel.Status); + } + + if (m_clientCommandChannel is not null) + { + status.AppendLine(); + status.AppendLine("Command Channel Status".CenterText(50)); + status.AppendLine("----------------------".CenterText(50)); + status.Append(m_clientCommandChannel.Status); + + #if !NET + status.AppendLine($" Using simple TCP client: {UseSimpleTcpClient}"); + #endif + } + + if (m_serverCommandChannel is not null) + { + status.AppendLine(); + status.AppendLine("Command Channel Status".CenterText(50)); + status.AppendLine("----------------------".CenterText(50)); + status.Append(m_serverCommandChannel.Status); + } + + status.Append(base.Status); + + return status.ToString(); + } + } + + /// + /// Gets a flag that determines if this uses an asynchronous connection. + /// + protected override bool UseAsyncConnect => true; + + /// + /// Gets or sets reference to data channel, attaching and/or detaching to events as needed. + /// + protected UdpClient? DataChannel + { + get => m_dataChannel; + set + { + if (m_dataChannel is not null) + { + // Detach from events on existing data channel reference + m_dataChannel.ConnectionException -= DataChannelConnectionException; + m_dataChannel.ConnectionAttempt -= DataChannelConnectionAttempt; + m_dataChannel.ReceiveData -= DataChannelReceiveData; + m_dataChannel.ReceiveDataException -= DataChannelReceiveDataException; + + if (m_dataChannel != value) + m_dataChannel.Dispose(); + } + + // Assign new data channel reference + m_dataChannel = value; + + if (m_dataChannel is not null) + { + // Attach to desired events on new data channel reference + m_dataChannel.ConnectionException += DataChannelConnectionException; + m_dataChannel.ConnectionAttempt += DataChannelConnectionAttempt; + m_dataChannel.ReceiveData += DataChannelReceiveData; + m_dataChannel.ReceiveDataException += DataChannelReceiveDataException; + } + } + } + + /// + /// Gets or sets reference to command channel, attaching and/or detaching to events as needed. + /// + protected IClient? ClientCommandChannel + { + get => m_clientCommandChannel; + set + { + if (m_clientCommandChannel is not null) + { + // Detach from events on existing command channel reference + m_clientCommandChannel.ConnectionAttempt -= ClientCommandChannelConnectionAttempt; + m_clientCommandChannel.ConnectionEstablished -= ClientCommandChannelConnectionEstablished; + m_clientCommandChannel.ConnectionException -= ClientCommandChannelConnectionException; + m_clientCommandChannel.ConnectionTerminated -= ClientCommandChannelConnectionTerminated; + m_clientCommandChannel.ReceiveData -= ClientCommandChannelReceiveData; + m_clientCommandChannel.ReceiveDataException -= ClientCommandChannelReceiveDataException; + m_clientCommandChannel.SendDataException -= ClientCommandChannelSendDataException; + + if (m_clientCommandChannel != value) + m_clientCommandChannel.Dispose(); + } + + // Assign new command channel reference + m_clientCommandChannel = value; + + if (m_clientCommandChannel is not null) + { + // Attach to desired events on new command channel reference + m_clientCommandChannel.ConnectionAttempt += ClientCommandChannelConnectionAttempt; + m_clientCommandChannel.ConnectionEstablished += ClientCommandChannelConnectionEstablished; + m_clientCommandChannel.ConnectionException += ClientCommandChannelConnectionException; + m_clientCommandChannel.ConnectionTerminated += ClientCommandChannelConnectionTerminated; + m_clientCommandChannel.ReceiveData += ClientCommandChannelReceiveData; + m_clientCommandChannel.ReceiveDataException += ClientCommandChannelReceiveDataException; + m_clientCommandChannel.SendDataException += ClientCommandChannelSendDataException; + } + } + } + + /// + /// Gets or sets reference to command channel, attaching and/or detaching to events as needed. + /// + /// + /// This handles reverse connectivity operations. + /// + protected IServer? ServerCommandChannel + { + get => m_serverCommandChannel; + set + { + if (m_serverCommandChannel is not null) + { + // Detach from events on existing command channel reference + m_serverCommandChannel.ClientConnected -= ServerCommandChannelClientConnected; + m_serverCommandChannel.ClientDisconnected -= ServerCommandChannelClientDisconnected; + m_serverCommandChannel.ClientConnectingException -= ServerCommandChannelClientConnectingException; + m_serverCommandChannel.ReceiveClientData -= ServerCommandChannelReceiveClientData; + m_serverCommandChannel.ReceiveClientDataException -= ServerCommandChannelReceiveClientDataException; + m_serverCommandChannel.SendClientDataException -= ServerCommandChannelSendClientDataException; + m_serverCommandChannel.ServerStarted -= ServerCommandChannelServerStarted; + m_serverCommandChannel.ServerStopped -= ServerCommandChannelServerStopped; + + if (m_serverCommandChannel != value) + m_serverCommandChannel.Dispose(); + } + + // Assign new command channel reference + m_serverCommandChannel = value; + + if (m_serverCommandChannel is not null) + { + // Attach to desired events on new command channel reference + m_serverCommandChannel.ClientConnected += ServerCommandChannelClientConnected; + m_serverCommandChannel.ClientDisconnected += ServerCommandChannelClientDisconnected; + m_serverCommandChannel.ClientConnectingException += ServerCommandChannelClientConnectingException; + m_serverCommandChannel.ReceiveClientData += ServerCommandChannelReceiveClientData; + m_serverCommandChannel.ReceiveClientDataException += ServerCommandChannelReceiveClientDataException; + m_serverCommandChannel.SendClientDataException += ServerCommandChannelSendClientDataException; + m_serverCommandChannel.ServerStarted += ServerCommandChannelServerStarted; + m_serverCommandChannel.ServerStopped += ServerCommandChannelServerStopped; + } + } + } + + /// + /// Gets the total number of measurements processed through this data publisher over the lifetime of the subscriber. + /// + public long LifetimeMeasurements { get; private set; } + + /// + /// Gets the minimum value of the measurements per second calculation. + /// + public long MinimumMeasurementsPerSecond { get; private set; } + + /// + /// Gets the maximum value of the measurements per second calculation. + /// + public long MaximumMeasurementsPerSecond { get; private set; } + + /// + /// Gets the average value of the measurements per second calculation. + /// + public long AverageMeasurementsPerSecond => m_measurementsPerSecondCount == 0L ? 0L : m_totalMeasurementsPerSecond / m_measurementsPerSecondCount; + + /// + /// Gets the minimum latency calculated over the full lifetime of the subscriber. + /// + public int LifetimeMinimumLatency => (int)Ticks.ToMilliseconds(m_lifetimeMinimumLatency); + + /// + /// Gets the maximum latency calculated over the full lifetime of the subscriber. + /// + public int LifetimeMaximumLatency => (int)Ticks.ToMilliseconds(m_lifetimeMaximumLatency); + + /// + /// Gets the average latency calculated over the full lifetime of the subscriber. + /// + public int LifetimeAverageLatency => m_lifetimeLatencyMeasurements == 0 ? -1 : (int)Ticks.ToMilliseconds(m_lifetimeTotalLatency / m_lifetimeLatencyMeasurements); + + /// + /// Gets real-time as determined by either the local clock or the latest measurement received. + /// + protected Ticks RealTime => UseLocalClockAsRealTime ? DateTime.UtcNow.Ticks : m_realTime; + + #endregion + + #region [ Methods ] + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + if (m_disposed) + return; + + try + { + if (!disposing) + return; + + DataLossInterval = 0.0D; + ClientCommandChannel = null; + ServerCommandChannel = null; + DataChannel = null; + + if (m_dataGapRecoverer is not null) + { + m_dataGapRecoverer.RecoveredMeasurements -= DataGapRecoverer_RecoveredMeasurements; + m_dataGapRecoverer.StatusMessage -= DataGapRecoverer_StatusMessage; + m_dataGapRecoverer.ProcessException -= DataGapRecoverer_ProcessException; + m_dataGapRecoverer.Dispose(); + m_dataGapRecoverer = null; + } + + if (m_runTimeLog is not null) + { + m_runTimeLog.ProcessException -= RunTimeLog_ProcessException; + m_runTimeLog.Dispose(); + m_runTimeLog = null; + } + + if (m_subscribedDevicesTimer is not null) + { + m_subscribedDevicesTimer.Elapsed -= SubscribedDevicesTimer_Elapsed; + m_subscribedDevicesTimer.Dispose(); + m_subscribedDevicesTimer = null; + } + } + finally + { + m_disposed = true; // Prevent duplicate dispose. + base.Dispose(disposing); // Call base class Dispose(). + } + } + + /// + /// Initializes . + /// + public override void Initialize() + { + base.Initialize(); + + Dictionary settings = Settings; + + // See if user has opted for different operational modes + if (settings.TryGetValue(nameof(OperationalModes), out string? setting) && Enum.TryParse(setting, true, out OperationalModes operationalModes)) + OperationalModes = operationalModes; + + // Set the security mode if explicitly defined + if (!settings.TryGetValue(nameof(SecurityMode), out setting) || !Enum.TryParse(setting, true, out SecurityMode securityMode)) + securityMode = SecurityMode.None; + + SecurityMode = securityMode; + + // Apply any version override (e.g., to downgrade to older version) + if (settings.TryGetValue(nameof(Version), out setting) && int.TryParse(setting, out int value) && value < 32) + Version = value; + + // Apply gateway compression mode to operational mode flags + if (settings.TryGetValue(nameof(CompressionModes), out setting) && Enum.TryParse(setting, true, out CompressionModes compressionModes)) + CompressionModes = compressionModes; + + // Check if output measurements should be filtered to only those belonging to the subscriber + FilterOutputMeasurements = !settings.TryGetValue(nameof(FilterOutputMeasurements), out setting) || setting.ParseBoolean(); + + // If no output measurements have been defined when filter output measurements is true, initialize outputs to an empty array, + // this will ensure that by default subscribed measurements are only those associated with the subscriber + if (FilterOutputMeasurements && (OutputMeasurements is null || OutputMeasurements.Length == 0)) + { + List outputMeasurements = []; + + foreach (Guid signalID in GetDeviceMeasurements()) + { + MeasurementKey key = MeasurementKey.LookUpBySignalID(signalID); + + if (key != MeasurementKey.Undefined) + outputMeasurements.Add(new Measurement { Metadata = key.Metadata }); + } + + OutputMeasurements = outputMeasurements.ToArray(); + } + + // Check if the subscriber supports real-time and historical processing + SupportsRealTimeProcessing = !settings.TryGetValue(nameof(SupportsRealTimeProcessing), out setting) || setting.ParseBoolean(); + m_supportsTemporalProcessing = settings.TryGetValue(nameof(SupportsTemporalProcessing), out setting) && setting.ParseBoolean(); + + // Check if measurements for this connection should be marked as "internal" - i.e., owned and allowed for proxy + if (settings.TryGetValue(nameof(Internal), out setting)) + Internal = setting.ParseBoolean(); + + // Check if user has explicitly defined the ReceiveInternalMetadata flag + if (settings.TryGetValue(nameof(ReceiveInternalMetadata), out setting)) + ReceiveInternalMetadata = setting.ParseBoolean(); + + // Check if user has explicitly defined the ReceiveExternalMetadata flag + if (settings.TryGetValue(nameof(ReceiveExternalMetadata), out setting)) + ReceiveExternalMetadata = setting.ParseBoolean(); + + // Check if user has explicitly defined the MutualSubscription flag + if (settings.TryGetValue(nameof(MutualSubscription), out setting) && setting.ParseBoolean()) + { + MutualSubscription = true; + ReceiveInternalMetadata = true; + ReceiveExternalMetadata = false; + FilterOutputMeasurements = false; + } + + // Check if user has defined a meta-data synchronization timeout + if (settings.TryGetValue(nameof(MetadataSynchronizationTimeout), out setting) && int.TryParse(setting, out int metadataSynchronizationTimeout)) + MetadataSynchronizationTimeout = metadataSynchronizationTimeout; + + // Check if user has defined a flag for using a transaction during meta-data synchronization + if (settings.TryGetValue(nameof(UseTransactionForMetadata), out setting)) + UseTransactionForMetadata = setting.ParseBoolean(); + + // Check if user has defined a flag for using identity inserts during meta-data synchronization + if (settings.TryGetValue(nameof(UseIdentityInsertsForMetadata), out setting)) + UseIdentityInsertsForMetadata = setting.ParseBoolean(); + + // Check if user has defined a flag for auto-deletion of CALC signals during meta-data synchronization + if (settings.TryGetValue(nameof(AutoDeleteCalculatedMeasurements), out setting)) + AutoDeleteCalculatedMeasurements = setting.ParseBoolean(); + + // Check if user has defined a flag for auto-deletion of ALRM signals during meta-data synchronization + if (settings.TryGetValue(nameof(AutoDeleteAlarmMeasurements), out setting)) + AutoDeleteAlarmMeasurements = setting.ParseBoolean(); + + // Check if user has defined a flag for synchronizing independent devices during meta-data synchronization + if (settings.TryGetValue(nameof(SyncIndependentDevices), out setting)) + SyncIndependentDevices = setting.ParseBoolean(); + + // Check if user has defined a flag for auto-enabling independently synced devices + if (settings.TryGetValue(nameof(AutoEnableIndependentlySyncedDevices), out setting)) + AutoEnableIndependentlySyncedDevices = setting.ParseBoolean(); + + // Check if user wants to request that publisher use millisecond resolution to conserve bandwidth + #pragma warning disable CS0618 // Type or member is obsolete + UseMillisecondResolution = !settings.TryGetValue(nameof(UseMillisecondResolution), out setting) || setting.ParseBoolean(); + #pragma warning restore CS0618 + + // Check if user has defined any meta-data filter expressions + if (settings.TryGetValue(nameof(MetadataFilters), out setting)) + MetadataFilters = setting; + + // Define auto connect setting + if (settings.TryGetValue(nameof(AutoConnect), out setting)) + { + AutoConnect = setting.ParseBoolean(); + + if (AutoConnect) + AutoSynchronizeMetadata = true; + } + + // Define the maximum allowed exceptions before resetting the connection + if (settings.TryGetValue(nameof(AllowedParsingExceptions), out setting)) + AllowedParsingExceptions = int.Parse(setting); + + // Define the window of time over which parsing exceptions are tolerated + if (settings.TryGetValue(nameof(ParsingExceptionWindow), out setting)) + ParsingExceptionWindow = Ticks.FromSeconds(double.Parse(setting)); + + // Check if synchronize meta-data is explicitly enabled or disabled + if (settings.TryGetValue(nameof(AutoSynchronizeMetadata), out setting)) + AutoSynchronizeMetadata = setting.ParseBoolean(); + else if (settings.TryGetValue("synchronizeMetadata", out setting)) + AutoSynchronizeMetadata = setting.ParseBoolean(); + + // Determine if source name prefixes should be applied during metadata synchronization + if (settings.TryGetValue(nameof(UseSourcePrefixNames), out setting)) + UseSourcePrefixNames = setting.ParseBoolean(); + + // Define data loss interval + if (settings.TryGetValue(nameof(DataLossInterval), out setting) && double.TryParse(setting, out double interval)) + DataLossInterval = interval; + + // Define buffer size + if (!settings.TryGetValue("bufferSize", out setting) || !int.TryParse(setting, out int bufferSize)) + bufferSize = DataPublisher.DefaultBufferSize; + + if (settings.TryGetValue(nameof(UseLocalClockAsRealTime), out setting)) + UseLocalClockAsRealTime = setting.ParseBoolean(); + + // Handle throttled subscription options + if (settings.TryGetValue(nameof(Throttled), out setting)) + Throttled = setting.ParseBoolean(); + + if (settings.TryGetValue(nameof(PublishInterval), out setting) && double.TryParse(setting, out interval)) + PublishInterval = interval; + + if (AutoConnect) + { + // Connect to local events when automatically engaging connection cycle + ConnectionAuthenticated += DataSubscriber_ConnectionAuthenticated; + MetaDataReceived += DataSubscriber_MetaDataReceived; + + // Update output measurements to include "subscribed" points + UpdateOutputMeasurements(true); + } + else if (AutoSynchronizeMetadata) + { + // Output measurements do not include "subscribed" points, + // but should still be filtered if applicable + TryFilterOutputMeasurements(); + } + + // Attempt to retrieve any defined command channel settings + Dictionary commandChannelSettings = settings.TryGetValue("commandChannel", out string? commandChannelConnectionString) ? commandChannelConnectionString.ParseKeyValuePairs() : settings; + + if (string.IsNullOrWhiteSpace(commandChannelConnectionString)) + commandChannelConnectionString = ConnectionString; + + bool serverBasedConnection = !commandChannelSettings.TryGetValue("server", out string? server) || string.IsNullOrWhiteSpace(server); + + #if !NET + if (settings.TryGetValue(nameof(UseSimpleTcpClient), out setting)) + UseSimpleTcpClient = setting.ParseBoolean(); + #endif + + if (securityMode == SecurityMode.TLS) + { + bool checkCertificateRevocation; + + if (!commandChannelSettings.TryGetValue("localCertificate", out string? localCertificate) || !File.Exists(localCertificate)) + localCertificate = GetLocalCertificate(); + + if (!commandChannelSettings.TryGetValue("remoteCertificate", out string? remoteCertificate) || !RemoteCertificateExists(ref remoteCertificate)) + throw new ArgumentException("The \"remoteCertificate\" setting must be defined and certificate file must exist when using TLS security mode."); + + if (!commandChannelSettings.TryGetValue("validPolicyErrors", out setting) || !Enum.TryParse(setting, out SslPolicyErrors validPolicyErrors)) + validPolicyErrors = SslPolicyErrors.None; + + if (!commandChannelSettings.TryGetValue("validChainFlags", out setting) || !Enum.TryParse(setting, out X509ChainStatusFlags validChainFlags)) + validChainFlags = X509ChainStatusFlags.NoError; + + if (commandChannelSettings.TryGetValue("checkCertificateRevocation", out setting) && !string.IsNullOrWhiteSpace(setting)) + checkCertificateRevocation = setting.ParseBoolean(); + else + checkCertificateRevocation = true; + + SimpleCertificateChecker certificateChecker = new(); + + // Set up certificate checker + certificateChecker.TrustedCertificates.Add(new X509Certificate2(FilePath.GetAbsolutePath(remoteCertificate))); + certificateChecker.ValidPolicyErrors = validPolicyErrors; + certificateChecker.ValidChainFlags = validChainFlags; + + if (serverBasedConnection) + { + // Create a new TLS server + TlsServer commandChannel = new() + { + ConfigurationString = commandChannelConnectionString, + PayloadAware = true, + PayloadMarker = null, + PayloadEndianOrder = EndianOrder.BigEndian, + MaxClientConnections = 1, // Subscriber can only serve a single publisher + CertificateFile = localCertificate, + CheckCertificateRevocation = checkCertificateRevocation, + CertificateChecker = certificateChecker, + RequireClientCertificate = true, + ReceiveBufferSize = bufferSize, + SendBufferSize = bufferSize, + NoDelay = true, + #if !NET + PersistSettings = false, + #endif + }; + + // Assign command channel server reference and attach to needed events + ServerCommandChannel = commandChannel; + } + else + { + // Create a new TLS client + TlsClient commandChannel = new() + { + ConnectionString = commandChannelConnectionString, + PayloadAware = true, + PayloadMarker = null, + PayloadEndianOrder = EndianOrder.BigEndian, + MaxConnectionAttempts = 1, + CertificateFile = FilePath.GetAbsolutePath(localCertificate!), + CheckCertificateRevocation = checkCertificateRevocation, + CertificateChecker = certificateChecker, + ReceiveBufferSize = bufferSize, + SendBufferSize = bufferSize, + NoDelay = true, + #if !NET + PersistSettings = false, + #endif + }; + + // Assign command channel client reference and attach to needed events + ClientCommandChannel = commandChannel; + } + } + else + { + if (serverBasedConnection) + { + // Create a new TCP server + TcpServer commandChannel = new() + { + ConfigurationString = commandChannelConnectionString, + PayloadAware = true, + PayloadMarker = null, + PayloadEndianOrder = EndianOrder.BigEndian, + MaxClientConnections = 1, // Subscriber can only serve a single publisher + ReceiveBufferSize = bufferSize, + SendBufferSize = bufferSize, + NoDelay = true, + #if !NET + PersistSettings = false, + #endif + }; + + // Assign command channel server reference and attach to needed events + ServerCommandChannel = commandChannel; + } + else + { + #if !NET + if (UseSimpleTcpClient) + { + // Create a new simple TCP client + TcpSimpleClient commandChannel = new() + { + ConnectionString = commandChannelConnectionString, + PayloadAware = true, + PayloadMarker = null, + PayloadEndianOrder = EndianOrder.BigEndian, + PersistSettings = false, + MaxConnectionAttempts = 1, + ReceiveBufferSize = bufferSize, + SendBufferSize = bufferSize, + NoDelay = true + }; + + // Assign command channel client reference and attach to needed events + ClientCommandChannel = commandChannel; + } + else + { + #endif + // Create a new TCP client + TcpClient commandChannel = new() + { + ConnectionString = commandChannelConnectionString, + PayloadAware = true, + PayloadMarker = null, + PayloadEndianOrder = EndianOrder.BigEndian, + MaxConnectionAttempts = 1, + ReceiveBufferSize = bufferSize, + SendBufferSize = bufferSize, + NoDelay = true, + #if !NET + PersistSettings = false, + #endif + }; + + // Assign command channel client reference and attach to needed events + ClientCommandChannel = commandChannel; + #if !NET + } + #endif + } + } + + if (commandChannelSettings.TryGetValue("DataChannelLocalPort", out setting) && ushort.TryParse(setting, out ushort dataChannelLocalPort)) + { + settings["commandChannel"] = commandChannelConnectionString; + settings["port"] = dataChannelLocalPort.ToString(); + } + + // Check for simplified compression setup flag + if (settings.TryGetValue("compression", out setting) && setting.ParseBoolean()) + { + CompressionModes |= CompressionModes.TSSC | CompressionModes.GZip; + OperationalModes |= OperationalModes.CompressPayloadData | OperationalModes.CompressMetadata | OperationalModes.CompressSignalIndexCache; + } + + // Get logging path, if any has been defined + if (settings.TryGetValue(nameof(LoggingPath), out setting)) + { + setting = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(setting)); + + if (Directory.Exists(setting)) + m_loggingPath = setting; + else + OnStatusMessage(MessageLevel.Info, $"Logging path \"{setting}\" not found, defaulting to \"{FilePath.GetAbsolutePath("")}\"...", flags: MessageFlags.UsageIssue); + } + + // Initialize data gap recovery processing, if requested + if (settings.TryGetValue("dataGapRecovery", out setting)) + { + if (m_clientCommandChannel is null) + { + m_dataGapRecoveryEnabled = false; + OnStatusMessage(MessageLevel.Warning, "Cannot use data gap recovery operations with a server-based data subscriber configuration. Data gap recovery will not be enabled.", "Data Subscriber Initialization", MessageFlags.UsageIssue); + } + else + { + // Make sure setting exists to allow user to by-pass data gap recovery at a configuration level + #if NET + SettingsSection systemSettings = ConfigSettings.Instance[ConfigSettings.SystemSettingsCategory]; + object? dataGapRecoveryEnabledSetting = systemSettings["DataGapRecoveryEnabled"]; + bool dataGapRecoveryEnabled = (dataGapRecoveryEnabledSetting?.ToString() ?? "false").ParseBoolean(); + #else + ConfigurationFile configFile = ConfigurationFile.Current; + CategorizedSettingsElementCollection systemSettings = configFile.Settings["systemSettings"]; + CategorizedSettingsElement dataGapRecoveryEnabledSetting = systemSettings["DataGapRecoveryEnabled"]; + bool dataGapRecoveryEnabled = dataGapRecoveryEnabledSetting is not null && dataGapRecoveryEnabledSetting.ValueAsBoolean(); + #endif + + // See if this node should process phasor source validation + if (dataGapRecoveryEnabled) + { + // Example connection string for data gap recovery: + // dataGapRecovery={enabled=true; recoveryStartDelay=10.0; minimumRecoverySpan=0.0; maximumRecoverySpan=3600.0} + Dictionary dataGapSettings = setting.ParseKeyValuePairs(); + + if (dataGapSettings.TryGetValue("enabled", out setting) && setting.ParseBoolean()) + { + // Remove dataGapRecovery connection setting from command channel connection string, if defined there. + // This will prevent any recursive data gap recovery operations from being established: + Dictionary connectionSettings = m_clientCommandChannel.ConnectionString.ParseKeyValuePairs(); + connectionSettings.Remove("dataGapRecovery"); + connectionSettings.Remove("synchronizeMetadata"); + connectionSettings.Remove(nameof(AutoConnect)); + connectionSettings.Remove(nameof(AutoSynchronizeMetadata)); + connectionSettings.Remove(nameof(OutputMeasurements)); + connectionSettings.Remove(nameof(BypassStatistics)); + connectionSettings.Remove(nameof(LoggingPath)); + + if (dataGapSettings.ContainsKey("server")) + connectionSettings.Remove("server"); + + // Note that the data gap recoverer will connect on the same command channel port as + // the real-time subscriber (TCP only) + m_dataGapRecoveryEnabled = true; + + m_dataGapRecoverer = new DataGapRecoverer + { + SourceConnectionName = Name, + DataSource = DataSource, + ConnectionString = string.Join("; ", $"autoConnect=false; synchronizeMetadata=false{(string.IsNullOrWhiteSpace(m_loggingPath) ? "" : "; loggingPath=" + m_loggingPath)}", dataGapSettings.JoinKeyValuePairs(), connectionSettings.JoinKeyValuePairs()), + FilterExpression = this.OutputMeasurementKeys().Select(key => key.SignalID.ToString()).ToDelimitedString(';') + }; + + m_dataGapRecoverer.RecoveredMeasurements += DataGapRecoverer_RecoveredMeasurements; + m_dataGapRecoverer.StatusMessage += DataGapRecoverer_StatusMessage; + m_dataGapRecoverer.ProcessException += DataGapRecoverer_ProcessException; + m_dataGapRecoverer.Initialize(); + } + else + { + m_dataGapRecoveryEnabled = false; + } + } + } + } + else + { + m_dataGapRecoveryEnabled = false; + } + + if (settings.TryGetValue(nameof(BypassStatistics), out setting) && setting.ParseBoolean()) + { + BypassStatistics = true; + } + else + { + void statisticsCalculated(object? sender, EventArgs args) + { + ResetMeasurementsPerSecondCounters(); + } + + StatisticsEngine.Register(this, "Subscriber", "SUB"); + StatisticsEngine.Calculated += statisticsCalculated; + Disposed += (_, _) => StatisticsEngine.Calculated -= statisticsCalculated; + } + + if (PersistConnectionForMetadata) + { + m_clientCommandChannel?.ConnectAsync(); + m_serverCommandChannel?.Start(); + } + + Initialized = true; + } + + // Initialize (or reinitialize) the output measurements associated with the data subscriber. + // Returns true if output measurements were updated, otherwise false if they remain the same. + private bool UpdateOutputMeasurements(bool initialCall = false) + { + IMeasurement[]? originalOutputMeasurements = OutputMeasurements; + + // Reapply output measurements if reinitializing - this way filter expressions and/or sourceIDs + // will be reapplied. This can be important after a meta-data refresh which may have added new + // measurements that could now be applicable as desired output measurements. + if (!initialCall) + { + if (Settings.TryGetValue("outputMeasurements", out string? setting)) + OutputMeasurements = ParseOutputMeasurements(DataSource, true, setting); + + #pragma warning disable CA2245 + OutputSourceIDs = OutputSourceIDs; + #pragma warning restore CA2245 + } + + // If active measurements are defined, attempt to defined desired subscription points from there + if ((SecurityMode == SecurityMode.TLS || FilterOutputMeasurements) && DataSource is not null && DataSource.Tables.Contains("ActiveMeasurements")) + { + try + { + // Filter to points associated with this subscriber that have been requested for subscription, are enabled and not owned locally + DataRow[] filteredRows = DataSource.Tables["ActiveMeasurements"]!.Select("Subscribed <> 0"); + List subscribedMeasurements = []; + + foreach (DataRow row in filteredRows) + { + // Create a new measurement for the provided field level information + Measurement measurement = new(); + + // Parse primary measurement identifier + Guid signalID = row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(); + + // Set measurement key if defined + MeasurementKey key = MeasurementKey.LookUpOrCreate(signalID, row["ID"].ToString()); + measurement.Metadata = key.Metadata; + subscribedMeasurements.Add(measurement); + } + + // Combine subscribed output measurement with any existing output measurement and return unique set + if (subscribedMeasurements.Count > 0) + OutputMeasurements = OutputMeasurements is null ? subscribedMeasurements.ToArray() : subscribedMeasurements.Concat(OutputMeasurements).Distinct().ToArray(); + } + catch (Exception ex) + { + // Errors here may not be catastrophic, this simply limits the auto-assignment of input measurement keys desired for subscription + OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to apply subscribed measurements to subscription filter: {ex.Message}", ex)); + } + } + + // Ensure that we are not attempting to subscribe to + // measurements that we know cannot be published + TryFilterOutputMeasurements(); + + // Determine if output measurements have changed + return originalOutputMeasurements?.CompareTo(OutputMeasurements, false) != 0; + } + + // When synchronizing meta-data, the publisher sends meta-data for all possible signals we can subscribe to. + // Here we check each signal defined in OutputMeasurements to determine whether that signal was defined in + // the published meta-data rather than blindly attempting to subscribe to all signals. + private void TryFilterOutputMeasurements() + { + if (!FilterOutputMeasurements) + return; + + try + { + HashSet measurementIDSet = GetDeviceMeasurements(); + + if (measurementIDSet.Count == 0 || OutputMeasurements is null) + { + OutputMeasurements = []; + return; + } + + OutputMeasurements = OutputMeasurements.Where(measurement => measurementIDSet.Contains(measurement.ID)).ToArray(); + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Error when filtering output measurements by device ID: {ex.Message}", ex)); + } + } + + private HashSet GetDeviceMeasurements() + { + if (DataSource is null || !DataSource.Tables.Contains("ActiveMeasurements")) + return []; + + Guid signalID = Guid.Empty; + + IEnumerable measurementIDs = DataSource.Tables["ActiveMeasurements"]! + .Select($"Convert(DeviceID, 'System.String') = '{ID}'") + .Where(row => Guid.TryParse(row["SignalID"].ToNonNullString(), out signalID)) + .Select(_ => signalID); + + return [..measurementIDs]; + } + + /// + /// Subscribes (or re-subscribes) to a data publisher for a set of data points. + /// + /// Configuration object that defines the subscription. + /// true if subscribe transmission was successful; otherwise false. + public bool Subscribe(SubscriptionInfo info) + { + StringBuilder connectionString = new(); + AssemblyInfo assemblyInfo = new(typeof(DataSubscriber).Assembly); + + connectionString.Append($"throttled={info.Throttled};"); + connectionString.Append($"publishInterval={info.PublishInterval};"); + connectionString.Append($"includeTime={info.IncludeTime};"); + connectionString.Append($"lagTime={info.LagTime};"); + connectionString.Append($"leadTime={info.LeadTime};"); + connectionString.Append($"useLocalClockAsRealTime={info.UseLocalClockAsRealTime};"); + connectionString.Append($"processingInterval={info.ProcessingInterval};"); + connectionString.Append($"useMillisecondResolution={info.UseMillisecondResolution};"); + connectionString.Append($"requestNaNValueFilter={info.RequestNaNValueFilter};"); + + Version version = assemblyInfo.Version ?? new Version(0, 0, 0); + #if NET + const string SourceLib = nameof(Gemstone); + #else + const string SourceLib = nameof(GSF); + #endif + connectionString.Append($"assemblyInfo={{source=STTP {SourceLib} Library ({assemblyInfo.Name}.dll); version={version.Major}.{version.Minor}.{version.Build}; updatedOn={assemblyInfo.BuildDate:yyyy-MM-dd HH:mm:ss} }};"); + + if (!string.IsNullOrWhiteSpace(info.FilterExpression)) + connectionString.Append($"filterExpression={{{info.FilterExpression}}};"); + + if (info.UdpDataChannel) + connectionString.Append($"dataChannel={{localport={info.DataChannelLocalPort}}};"); + + if (!string.IsNullOrWhiteSpace(info.StartTime)) + connectionString.Append($"startTimeConstraint={info.StartTime};"); + + if (!string.IsNullOrWhiteSpace(info.StopTime)) + connectionString.Append($"stopTimeConstraint={info.StopTime};"); + + if (!string.IsNullOrWhiteSpace(info.ConstraintParameters)) + connectionString.Append($"timeConstraintParameters={info.ConstraintParameters};"); + + if (!string.IsNullOrWhiteSpace(info.ExtraConnectionStringParameters)) + connectionString.Append($"{info.ExtraConnectionStringParameters};"); + + // Make sure not to monitor for data loss any faster than down-sample time on throttled connections - additionally + // you will want to make sure data stream monitor is twice lag-time to allow time for initial points to arrive. + if (info.Throttled && m_dataStreamMonitor is not null && m_dataStreamMonitor.Interval / 1000.0D < info.LagTime) + m_dataStreamMonitor.Interval = (int)(2.0D * info.LagTime * 1000.0D); + + // Set millisecond resolution member variable for compact measurement parsing + #pragma warning disable 618 + UseMillisecondResolution = info.UseMillisecondResolution; + #pragma warning restore 618 + + return Subscribe(info.UseCompactMeasurementFormat, connectionString.ToString()); + } + + /// + /// Subscribes (or re-subscribes) to a data publisher for an unsynchronized set of data points. + /// + /// Boolean value that determines if the compact measurement format should be used. Set to false for full fidelity measurement serialization; otherwise set to true for bandwidth conservation. + /// Boolean value that determines if data should be throttled at a set transmission interval or sent on change. + /// Filtering expression that defines the measurements that are being subscribed. + /// Desired UDP return data channel connection string to use for data packet transmission. Set to null to use TCP channel for data transmission. + /// Boolean value that determines if time is a necessary component in streaming data. + /// When is true, defines the data transmission speed in seconds (can be sub-second). + /// When is true, defines the allowed time deviation tolerance to real-time in seconds (can be sub-second). + /// When is true, defines boolean value that determines whether to use the local clock time as real-time. Set to false to use latest received measurement timestamp as real-time. + /// Defines a relative or exact start time for the temporal constraint to use for historical playback. + /// Defines a relative or exact stop time for the temporal constraint to use for historical playback. + /// Defines any temporal parameters related to the constraint to use for historical playback. + /// Defines the desired processing interval milliseconds, i.e., historical play back speed, to use when temporal constraints are defined. + /// Defines the interval, in seconds, at which data should be published when using a throttled subscription. + /// true if subscribe transmission was successful; otherwise false. + /// + /// + /// When the or temporal processing constraints are defined (i.e., not null), this + /// specifies the start and stop time over which the subscriber session will process data. Passing in null for the + /// and specifies the subscriber session will process data in standard, i.e., real-time, operation. + /// + /// + /// Except for the values of -1 and 0, the value specifies the desired historical playback data + /// processing interval in milliseconds. This is basically a delay, or timer interval, over which to process data. Setting this value + /// to -1 means to use the default processing interval while setting the value to 0 means to process data as fast as possible. + /// + /// + /// The and parameters can be specified in one of the + /// following formats: + /// + /// + /// Time Format + /// Format Description + /// + /// + /// 12-30-2000 23:59:59.033 + /// Absolute date and time. + /// + /// + /// * + /// Evaluates to . + /// + /// + /// *-20s + /// Evaluates to 20 seconds before . + /// + /// + /// *-10m + /// Evaluates to 10 minutes before . + /// + /// + /// *-1h + /// Evaluates to 1 hour before . + /// + /// + /// *-1d + /// Evaluates to 1 day before . + /// + /// + /// + /// + [Obsolete("Preferred method uses SubscriptionInfo object to subscribe.", false)] + public virtual bool Subscribe(bool compactFormat, bool throttled, string filterExpression, string? dataChannel = null, bool includeTime = true, double lagTime = 10.0D, double leadTime = 5.0D, bool useLocalClockAsRealTime = false, string? startTime = null, string? stopTime = null, string? constraintParameters = null, int processingInterval = -1, double publishInterval = -1.0D) + { + int port = 0; + + if (!string.IsNullOrWhiteSpace(dataChannel)) + { + Dictionary settings = dataChannel.ParseKeyValuePairs(); + + if (settings.TryGetValue("port", out string? setting) || settings.TryGetValue("localPort", out setting)) + #pragma warning disable CA1806 + int.TryParse(setting, out port); + #pragma warning restore CA1806 + } + + return Subscribe(new SubscriptionInfo + { + UseCompactMeasurementFormat = compactFormat, + Throttled = throttled, + PublishInterval = publishInterval, + UdpDataChannel = port > 0, + DataChannelLocalPort = port, + FilterExpression = filterExpression, + IncludeTime = includeTime, + LagTime = lagTime, + LeadTime = leadTime, + UseLocalClockAsRealTime = useLocalClockAsRealTime, + StartTime = startTime, + StopTime = stopTime, + ConstraintParameters = constraintParameters, + ProcessingInterval = processingInterval, + UseMillisecondResolution = UseMillisecondResolution // When used as adapter, use configured option + }); + } + + /// + /// Subscribes (or re-subscribes) to a data publisher for a set of data points. + /// + /// Boolean value that determines if the compact measurement format should be used. Set to false for full fidelity measurement serialization; otherwise set to true for bandwidth conservation. + /// Connection string that defines required and optional parameters for the subscription. + /// true if subscribe transmission was successful; otherwise false. + public virtual bool Subscribe(bool compactFormat, string connectionString) + { + bool success = false; + + if (!string.IsNullOrWhiteSpace(connectionString)) + { + try + { + // Parse connection string to see if it contains a data channel definition + Dictionary settings = connectionString.ParseKeyValuePairs(); + UdpClient? dataChannel = null; + + // Track specified time inclusion for later deserialization + m_includeTime = !settings.TryGetValue("includeTime", out string? setting) || setting.ParseBoolean(); + + settings.TryGetValue("dataChannel", out setting); + + if (!string.IsNullOrWhiteSpace(setting)) + { + if ((CompressionModes & CompressionModes.TSSC) > 0) + { + // TSSC is a stateful compression algorithm which will not reliably support UDP + OnStatusMessage(MessageLevel.Warning, "Cannot use TSSC compression mode with UDP - special compression mode disabled"); + + // Disable TSSC compression processing + CompressionModes &= ~CompressionModes.TSSC; + } + + dataChannel = new UdpClient(setting) + { + ReceiveBufferSize = ushort.MaxValue, + MaxConnectionAttempts = -1 + }; + + dataChannel.ConnectAsync(); + } + + // Assign data channel client reference and attach to needed events + DataChannel = dataChannel; + + // Setup subscription packet + using BlockAllocatedMemoryStream buffer = new(); + DataPacketFlags flags = DataPacketFlags.NoFlags; + + if (compactFormat) + flags |= DataPacketFlags.Compact; + + // Write data packet flags into buffer + buffer.WriteByte((byte)flags); + + // Get encoded bytes of connection string + byte[] bytes = Encoding.GetBytes(connectionString); + + // Write encoded connection string length into buffer + buffer.Write(BigEndian.GetBytes(bytes.Length), 0, 4); + + // Encode connection string into buffer + buffer.Write(bytes, 0, bytes.Length); + + // Send subscribe server command with associated command buffer + success = SendServerCommand(ServerCommand.Subscribe, buffer.ToArray()); + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Error, new InvalidOperationException($"Exception occurred while trying to make publisher subscription: {ex.Message}", ex)); + } + } + else + { + OnProcessException(MessageLevel.Error, new InvalidOperationException("Cannot make publisher subscription without a connection string.")); + } + + return success; + } + + /// + /// Subscribes to a data publisher based on currently configured adapter settings. + /// + /// true if subscribe command was sent successfully; otherwise false. + [AdapterCommand("Subscribes to data publisher.")] + [EditorBrowsable(EditorBrowsableState.Advanced)] // Method exists for remote console execution + public virtual bool Subscribe() + { + return SubscribeToOutputMeasurements(!m_metadataRefreshPending); + } + + /// + /// Unsubscribes from a data publisher. + /// + /// true if unsubscribe command was sent successfully; otherwise false. + [AdapterCommand("Unsubscribes from data publisher.")] + public virtual bool Unsubscribe() + { + return SendServerCommand(ServerCommand.Unsubscribe); + } + + /// + /// Returns the measurements signal IDs that were authorized after the last successful subscription request. + /// + [AdapterCommand("Gets authorized signal IDs from last subscription request.")] + [Label("Get Authorized Signal IDs")] + public virtual Guid[] GetAuthorizedSignalIDs() + { + lock (m_signalIndexCacheLock) + return m_signalIndexCache?[m_cacheIndex] is null ? [] : m_signalIndexCache[m_cacheIndex]!.AuthorizedSignalIDs; + } + + /// + /// Returns the measurements signal IDs that were unauthorized after the last successful subscription request. + /// + [AdapterCommand("Gets unauthorized signal IDs from last subscription request.")] + [Label("Get Unauthorized SignalIDs")] + public virtual Guid[] GetUnauthorizedSignalIDs() + { + lock (m_signalIndexCacheLock) + return m_signalIndexCache?[m_cacheIndex] is null ? [] : m_signalIndexCache[m_cacheIndex]!.UnauthorizedSignalIDs; + } + + /// + /// Resets the counters for the lifetime statistics without interrupting the adapter's operations. + /// + [AdapterCommand("Resets the counters for the lifetime statistics without interrupting the adapter's operations.")] + [Label("Reset Lifetime Counters")] + public virtual void ResetLifetimeCounters() + { + LifetimeMeasurements = 0L; + TotalBytesReceived = 0L; + m_lifetimeTotalLatency = 0L; + m_lifetimeMinimumLatency = 0L; + m_lifetimeMaximumLatency = 0L; + m_lifetimeLatencyMeasurements = 0L; + } + + /// + /// Initiate a meta-data refresh. + /// + [AdapterCommand("Initiates a meta-data refresh.")] + [Label("Refresh Metadata")] + public virtual void RefreshMetadata() + { + SendServerCommand(ServerCommand.MetaDataRefresh, MetadataFilters); + } + + /// + /// Log a data gap for data gap recovery. + /// + /// The string representing the data gap. + [AdapterCommand("Logs a data gap for data gap recovery.")] + [Label("Log Data Gap")] + [Parameter(nameof(timeString), "Time String", "The string representing the data gap.")] + public virtual void LogDataGap(string timeString) + { + DateTimeOffset end = default; + string[] split = timeString.Split(';'); + + if (!m_dataGapRecoveryEnabled) + throw new InvalidOperationException("Data gap recovery is not enabled."); + + if (split.Length != 2) + throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733"); + + string startTime = split[0]; + string endTime = split[1]; + + bool parserSuccessful = + DateTimeOffset.TryParse(startTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out DateTimeOffset start) && + DateTimeOffset.TryParse(endTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out end); + + if (!parserSuccessful) + throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733"); + + m_dataGapRecoverer?.LogDataGap(start, end, true); + } + + /// + /// Remove a data gap from data gap recovery. + /// + /// The string representing the data gap. + [AdapterCommand("Removes a data gap from data gap recovery.")] + [Label("Remove Data Gap")] + [Parameter(nameof(timeString), "Time String", "The string representing the data gap.")] + public virtual string RemoveDataGap(string timeString) + { + DateTimeOffset end = default; + string[] split = timeString.Split(';'); + + if (!m_dataGapRecoveryEnabled) + throw new InvalidOperationException("Data gap recovery is not enabled."); + + if (split.Length != 2) + throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733"); + + string startTime = split[0]; + string endTime = split[1]; + + bool parserSuccessful = + DateTimeOffset.TryParse(startTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out DateTimeOffset start) && + DateTimeOffset.TryParse(endTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out end); + + if (!parserSuccessful) + throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733"); + + if (m_dataGapRecoverer?.RemoveDataGap(start, end) ?? false) + return "Data gap successfully removed."; + + return "Data gap not found."; + } + + /// + /// Displays the contents of the outage log. + /// + /// The contents of the outage log. + [AdapterCommand("Displays data gaps queued for data gap recovery.")] + [Label("Dump Outage Log")] + public virtual string DumpOutageLog() + { + if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null) + return Environment.NewLine + m_dataGapRecoverer.DumpOutageLog(); + + throw new InvalidOperationException("Data gap recovery not enabled"); + } + + /// + /// Gets the status of the temporal used by the data gap recovery module. + /// + /// Status of the temporal used by the data gap recovery module. + [AdapterCommand("Gets the status of the temporal subscription used by the data gap recovery module.")] + [Label("Get Data Gap Recovery Subscription Status")] + public virtual string GetDataGapRecoverySubscriptionStatus() + { + if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null) + return m_dataGapRecoverer.TemporalSubscriptionStatus; + + return "Data gap recovery not enabled"; + } + + /// + /// Spawn meta-data synchronization. + /// + /// to use for synchronization. + /// + /// This method makes sure only one meta-data synchronization happens at a time. + /// + public void SynchronizeMetadata(DataSet metadata) + { + try + { + m_receivedMetadata = metadata; + #if NET + m_synchronizeMetadataOperation.RunAsync(); + #else + m_synchronizeMetadataOperation.RunOnceAsync(); + #endif + } + catch (Exception ex) + { + // Process exception for logging + OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to queue meta-data synchronization: {ex.Message}", ex)); + } + } + + /// + /// Sends a server command to the publisher connection with associated data. + /// + /// to send. + /// String based command data to send to server. + /// true if transmission was successful; otherwise false. + public virtual bool SendServerCommand(ServerCommand commandCode, string? message) + { + if (string.IsNullOrWhiteSpace(message)) + return SendServerCommand(commandCode); + + using BlockAllocatedMemoryStream buffer = new(); + byte[] bytes = Encoding.GetBytes(message); + + buffer.Write(BigEndian.GetBytes(bytes.Length), 0, 4); + buffer.Write(bytes, 0, bytes.Length); + + return SendServerCommand(commandCode, buffer.ToArray()); + } + + /// + /// Sends a server command to the publisher connection. + /// + /// to send. + /// Optional command data to send. + /// true if transmission was successful; otherwise false. + public virtual bool SendServerCommand(ServerCommand commandCode, byte[]? data = null) + { + if (m_clientCommandChannel?.CurrentState == ClientState.Connected || m_serverCommandChannel?.CurrentState == ServerState.Running && m_activeClientID != Guid.Empty) + { + try + { + using BlockAllocatedMemoryStream commandPacket = new(); + // Write command code into command packet + commandPacket.WriteByte((byte)commandCode); + + // Write command buffer into command packet + if (data is not null && data.Length > 0) + commandPacket.Write(data, 0, data.Length); + + // Send command packet to publisher + m_clientCommandChannel?.SendAsync(commandPacket.ToArray(), 0, (int)commandPacket.Length); + m_serverCommandChannel?.SendToAsync(m_activeClientID, commandPacket.ToArray(), 0, (int)commandPacket.Length); + m_metadataRefreshPending = commandCode == ServerCommand.MetaDataRefresh; + + return true; + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Error, new InvalidOperationException($"Exception occurred while trying to send server command \"{commandCode}\" to publisher: {ex.Message}", ex)); + } + } + else + OnProcessException(MessageLevel.Error, new InvalidOperationException($"Subscriber is currently unconnected. Cannot send server command \"{commandCode}\" to publisher.")); + + return false; + } + + /// + /// Attempts to connect to this . + /// + protected override void AttemptConnection() + { + if (!this.TemporalConstraintIsDefined() && !SupportsRealTimeProcessing) + return; + + long now = UseLocalClockAsRealTime ? DateTime.UtcNow.Ticks : 0L; + List>? statisticsHelpers = m_statisticsHelpers; + + #if NET + m_registerStatisticsOperation.RunAsync(); + #else + m_registerStatisticsOperation.RunOnceAsync(); + #endif + m_expectedBufferBlockSequenceNumber = 0u; + m_commandChannelConnectionAttempts = 0; + m_dataChannelConnectionAttempts = 0; + + m_subscribed = false; + m_keyIVs = null; + TotalBytesReceived = 0L; + m_monitoredBytesReceived = 0L; + m_lastBytesReceived = 0; + m_lastReceivedAt = DateTime.MinValue; + + if (!PersistConnectionForMetadata) + { + m_clientCommandChannel?.ConnectAsync(); + m_serverCommandChannel?.Start(); + } + else + { + OnConnected(); + } + + if (PersistConnectionForMetadata && CommandChannelConnected) + SubscribeToOutputMeasurements(true); + + if (UseLocalClockAsRealTime && m_subscribedDevicesTimer is null) + { + m_subscribedDevicesTimer = Common.TimerScheduler.CreateTimer(1000); + m_subscribedDevicesTimer.Elapsed += SubscribedDevicesTimer_Elapsed; + } + + if (statisticsHelpers is not null) + { + m_realTime = 0L; + m_lastStatisticsHelperUpdate = 0L; + + foreach (DeviceStatisticsHelper statisticsHelper in statisticsHelpers) + statisticsHelper.Reset(now); + } + + if (UseLocalClockAsRealTime) + m_subscribedDevicesTimer?.Start(); + } + + /// + /// Attempts to disconnect from this . + /// + protected override void AttemptDisconnection() + { + // Unregister device statistics + #if NET + m_registerStatisticsOperation.RunAsync(); + #else + m_registerStatisticsOperation.RunOnceAsync(); + #endif + + // Stop data stream monitor + if (m_dataStreamMonitor is not null) + m_dataStreamMonitor.Enabled = false; + + // Disconnect command channel + if (!PersistConnectionForMetadata) + { + m_clientCommandChannel?.Disconnect(); + m_serverCommandChannel?.Stop(); + } + + m_activeClientID = Guid.Empty; + m_subscribedDevicesTimer?.Stop(); + m_metadataRefreshPending = false; + } + + /// + /// Gets a short one-line status of this . + /// + /// Maximum length of the status message. + /// Text of the status message. + public override string GetShortStatus(int maxLength) + { + if (m_clientCommandChannel?.CurrentState == ClientState.Connected) + return $"Subscriber connection has received {ProcessedMeasurements:N0} data points".CenterText(maxLength); + + if (m_serverCommandChannel?.CurrentState == ServerState.Running && m_activeClientID != Guid.Empty) + return $"Subscriber server-based connection has received {ProcessedMeasurements:N0} points".CenterText(maxLength); + + return "Subscriber is not connected.".CenterText(maxLength); + } + + /// + /// Get message from string based response. + /// + /// Response buffer. + /// Start index of response message. + /// Length of response message. + /// Decoded response string. + protected string InterpretResponseMessage(byte[] buffer, int startIndex, int length) + { + return Encoding.GetString(buffer, startIndex, length); + } + + // Restarts the subscriber. + private void Restart() + { + try + { + base.Start(); + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Warning, ex); + } + } + + private void ProcessServerResponse(byte[]? buffer, int length) + { + // Currently this work is done on the async socket completion thread, make sure work to be done is timely and if the response processing + // is coming in via the command channel and needs to send a command back to the server, it should be done on a separate thread... + if (buffer is null || length <= 0) + return; + + int startIndex = 0; + + while (startIndex < length) + { + try + { + Dictionary>? subscribedDevicesLookup; + DeviceStatisticsHelper? statisticsHelper; + + ServerResponse responseCode = (ServerResponse)buffer[startIndex]; + ServerCommand commandCode = (ServerCommand)buffer[startIndex + 1]; + int responseLength = BigEndian.ToInt32(buffer, startIndex + 2); + int responseIndex = startIndex + DataPublisher.ClientResponseHeaderSize; + byte[][][] keyIVs; + + startIndex = responseIndex + responseLength; + + // Disconnect any established UDP data channel upon successful unsubscribe + if (commandCode == ServerCommand.Unsubscribe && responseCode == ServerResponse.Succeeded) + DataChannel = null; + + if (!IsUserCommand(commandCode)) + OnReceivedServerResponse(responseCode, commandCode); + else + OnReceivedUserCommandResponse(commandCode, responseCode, buffer, responseIndex, responseLength); + + switch (responseCode) + { + case ServerResponse.Succeeded: + switch (commandCode) + { + case ServerCommand.Subscribe: + OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}"); + m_subscribed = true; + break; + case ServerCommand.Unsubscribe: + OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}"); + m_subscribed = false; + if (m_dataStreamMonitor is not null) + m_dataStreamMonitor.Enabled = false; + break; + case ServerCommand.RotateCipherKeys: + OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}"); + break; + case ServerCommand.MetaDataRefresh: + OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": latest meta-data received."); + OnMetaDataReceived(DeserializeMetadata(buffer.BlockCopy(responseIndex, responseLength))); + m_metadataRefreshPending = false; + break; + } + + break; + case ServerResponse.Failed: + OnStatusMessage(MessageLevel.Info, $"Failure code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}"); + + if (commandCode == ServerCommand.MetaDataRefresh) + m_metadataRefreshPending = false; + break; + case ServerResponse.DataPacket: + { + long now = DateTime.UtcNow.Ticks; + + // Deserialize data packet + List measurements = []; + Ticks timestamp = default; + + if (TotalBytesReceived == 0) + { + // At the point when data is being received, data monitor should be enabled + if (!(m_dataStreamMonitor?.Enabled ?? false)) + m_dataStreamMonitor!.Enabled = true; + + // Establish run-time log for subscriber + if (AutoConnect || m_dataGapRecoveryEnabled) + { + if (m_runTimeLog is null) + { + m_runTimeLog = new RunTimeLog { FileName = GetLoggingPath($"{Name}_RunTimeLog.txt") }; + m_runTimeLog.ProcessException += RunTimeLog_ProcessException; + m_runTimeLog.Initialize(); + } + else + { + // Mark the start of any data transmissions + m_runTimeLog.StartTime = DateTimeOffset.UtcNow; + m_runTimeLog.Enabled = true; + } + } + + // The duration between last disconnection and start of data transmissions + // represents a gap in data - if data gap recovery is enabled, we log + // this as a gap for recovery: + if (m_dataGapRecoveryEnabled) + m_dataGapRecoverer?.LogDataGap(m_runTimeLog!.StopTime, DateTimeOffset.UtcNow); + } + + // Track total data packet bytes received from any channel + TotalBytesReceived += m_lastBytesReceived; + m_monitoredBytesReceived += m_lastBytesReceived; + + // Get data packet flags + DataPacketFlags flags = (DataPacketFlags)buffer[responseIndex]; + responseIndex++; + + SignalIndexCache? signalIndexCache; + bool compactMeasurementFormat = (byte)(flags & DataPacketFlags.Compact) > 0; + bool compressedPayload = (byte)(flags & DataPacketFlags.Compressed) > 0; + int cipherIndex = (flags & DataPacketFlags.CipherIndex) > 0 ? 1 : 0; + int cacheIndex = (flags & DataPacketFlags.CacheIndex) > 0 ? 1 : 0; + byte[] packet = buffer; + int packetLength = responseLength - 1; + + lock (m_signalIndexCacheLock) + signalIndexCache = m_signalIndexCache?[cacheIndex]; + + // Decrypt data packet payload if keys are available + if (m_keyIVs is not null) + { + // Get a local copy of volatile keyIVs reference since this can change at any time + keyIVs = m_keyIVs; + + // Decrypt payload portion of data packet + packet = Common.SymmetricAlgorithm.Decrypt(packet, responseIndex, packetLength, keyIVs[cipherIndex][0], keyIVs[cipherIndex][1]); + responseIndex = 0; + packetLength = packet.Length; + } + + // Deserialize number of measurements that follow + int count = BigEndian.ToInt32(packet, responseIndex); + responseIndex += 4; + packetLength -= 4; + + if (compressedPayload) + { + if (CompressionModes.HasFlag(CompressionModes.TSSC)) + { + if (signalIndexCache is not null) + { + try + { + // Decompress TSSC serialized measurements from payload + ParseTSSCMeasurements(packet, packetLength, signalIndexCache, ref responseIndex, measurements); + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Error, new InvalidOperationException($"Decompression failure: (Decoded {measurements.Count} of {count} measurements) - {ex.Message}", ex)); + } + } + } + else + { + OnProcessException(MessageLevel.Error, new InvalidOperationException("Decompression failure: Unexpected compression type in use - STTP currently only supports TSSC payload compression")); + } + } + else + { + // Deserialize measurements + for (int i = 0; i < count; i++) + { + if (!compactMeasurementFormat) + { + #if NET + throw new NotSupportedException("Full measurement format not supported in Gemstone STTP implementation"); + #else + // Deserialize full measurement format + SerializableMeasurement measurement = new(Encoding); + responseIndex += measurement.ParseBinaryImage(packet, responseIndex, length - responseIndex); + measurements.Add(measurement); + #endif + } + // ReSharper disable once RedundantIfElseBlock + else if (signalIndexCache is not null) + { + #pragma warning disable 618 + bool useMillisecondResolution = UseMillisecondResolution; + #pragma warning restore 618 + + // Deserialize compact measurement format + CompactMeasurement measurement = new(signalIndexCache, m_includeTime, m_baseTimeOffsets, m_timeIndex, useMillisecondResolution); + responseIndex += measurement.ParseBinaryImage(packet, responseIndex, length - responseIndex); + + // Apply timestamp from frame if not included in transmission + if (!measurement.IncludeTime) + measurement.Timestamp = timestamp; + + measurements.Add(measurement); + } + else if (m_lastMissingCacheWarning + MissingCacheWarningInterval < now) + { + // Warning message for missing signal index cache + if (m_lastMissingCacheWarning != 0L) + OnStatusMessage(MessageLevel.Error, "Signal index cache has not arrived. No compact measurements can be parsed."); + + m_lastMissingCacheWarning = now; + } + } + } + + // Calculate statistics + subscribedDevicesLookup = m_subscribedDevicesLookup; + statisticsHelper = null; + + if (subscribedDevicesLookup is not null) + { + IEnumerable?, IMeasurement>> deviceGroups = measurements + .Where(measurement => subscribedDevicesLookup.TryGetValue(measurement.ID, out statisticsHelper)) + .Select(measurement => Tuple.Create(statisticsHelper, measurement)) + .ToList() + .GroupBy(tuple => tuple.Item1, tuple => tuple.Item2); + + foreach (IGrouping?, IMeasurement> deviceGroup in deviceGroups) + { + statisticsHelper = deviceGroup.Key; + + foreach (IGrouping frame in deviceGroup.GroupBy(measurement => measurement.Timestamp)) + { + // Determine the number of measurements received with valid values + const MeasurementStateFlags ErrorFlags = MeasurementStateFlags.BadData | MeasurementStateFlags.BadTime | MeasurementStateFlags.SystemError; + + static bool hasError(MeasurementStateFlags stateFlags) + { + return (stateFlags & ErrorFlags) != MeasurementStateFlags.Normal; + } + + int measurementsReceived = frame.Count(measurement => !double.IsNaN(measurement.Value)); + int measurementsWithError = frame.Count(measurement => !double.IsNaN(measurement.Value) && hasError(measurement.StateFlags)); + + IMeasurement? statusFlags = null; + IMeasurement? frequency = null; + IMeasurement? deltaFrequency = null; + + // Attempt to update real-time + if (!UseLocalClockAsRealTime && frame.Key > m_realTime) + m_realTime = frame.Key; + + // Search the frame for status flags, frequency, and delta frequency + foreach (IMeasurement measurement in frame) + { + if (measurement.ID == statisticsHelper?.Device.StatusFlagsID) + statusFlags = measurement; + else if (measurement.ID == statisticsHelper?.Device.FrequencyID) + frequency = measurement; + else if (measurement.ID == statisticsHelper?.Device.DeltaFrequencyID) + deltaFrequency = measurement; + } + + // If we are receiving status flags for this device, + // count the data quality, time quality, and device errors + if (statusFlags is not null) + { + if (statisticsHelper is not null) + { + uint commonStatusFlags = (uint)statusFlags.Value; + + if ((commonStatusFlags & (uint)Bits.Bit19) > 0) + statisticsHelper.Device.DataQualityErrors++; + + if ((commonStatusFlags & (uint)Bits.Bit18) > 0) + statisticsHelper.Device.TimeQualityErrors++; + + if ((commonStatusFlags & (uint)Bits.Bit16) > 0) + statisticsHelper.Device.DeviceErrors++; + } + + measurementsReceived--; + + if (hasError(statusFlags.StateFlags)) + measurementsWithError--; + } + + // Zero is not a valid value for frequency. + // If frequency is zero, invalidate both frequency and delta frequency + if (frequency is not null) + { + if (!this.TemporalConstraintIsDefined()) + statisticsHelper?.MarkDeviceTimestamp(frequency.Timestamp); + + if (frequency.Value == 0.0D) + { + if (deltaFrequency is null || double.IsNaN(deltaFrequency.Value)) + measurementsReceived--; + else + measurementsReceived -= 2; + + if (hasError(frequency.StateFlags)) + { + if (deltaFrequency is null || double.IsNaN(deltaFrequency.Value)) + measurementsWithError--; + else + measurementsWithError -= 2; + } + } + } + + // Track the number of measurements received + statisticsHelper?.AddToMeasurementsReceived(measurementsReceived); + statisticsHelper?.AddToMeasurementsWithError(measurementsWithError); + } + } + } + + OnNewMeasurements(measurements); + + // Gather statistics on received data + DateTime timeReceived = RealTime; + + if (!UseLocalClockAsRealTime && timeReceived.Ticks - m_lastStatisticsHelperUpdate > Ticks.PerSecond) + { + UpdateStatisticsHelpers(); + m_lastStatisticsHelperUpdate = m_realTime; + } + + LifetimeMeasurements += measurements.Count; + UpdateMeasurementsPerSecond(timeReceived, measurements.Count); + + foreach (IMeasurement measurement in measurements) + { + long latency = timeReceived.Ticks - (long)measurement.Timestamp; + + // Throw out latencies that exceed one hour as invalid + if (Math.Abs(latency) > Time.SecondsPerHour * Ticks.PerSecond) + continue; + + if (m_lifetimeMinimumLatency > latency || m_lifetimeMinimumLatency == 0) + m_lifetimeMinimumLatency = latency; + + if (m_lifetimeMaximumLatency < latency || m_lifetimeMaximumLatency == 0) + m_lifetimeMaximumLatency = latency; + + m_lifetimeTotalLatency += latency; + m_lifetimeLatencyMeasurements++; + } + + break; + } + case ServerResponse.BufferBlock: + { + // Buffer block received - wrap as a buffer block measurement and expose back to consumer + uint sequenceNumber = BigEndian.ToUInt32(buffer, responseIndex); + int bufferCacheIndex = (int)(sequenceNumber - m_expectedBufferBlockSequenceNumber); + int signalCacheIndex = Version > 1 ? buffer[responseIndex + 4] : 0; + + // Check if this buffer block has already been processed (e.g., mistaken retransmission due to timeout) + if (bufferCacheIndex >= 0 && (bufferCacheIndex >= m_bufferBlockCache.Count || m_bufferBlockCache[bufferCacheIndex] is null)) + { + // Send confirmation that buffer block is received + SendServerCommand(ServerCommand.ConfirmBufferBlock, buffer.BlockCopy(responseIndex, 4)); + + if (Version > 1) + responseIndex += 1; + + // Get measurement key from signal index cache + int signalIndex = BigEndian.ToInt32(buffer, responseIndex + 4); + + SignalIndexCache? signalIndexCache; + + lock (m_signalIndexCacheLock) + signalIndexCache = m_signalIndexCache?[signalCacheIndex]; + + if (signalIndexCache is null || !signalIndexCache.Reference.TryGetValue(signalIndex, out MeasurementKey? measurementKey)) + throw new InvalidOperationException($"Failed to find associated signal identification for runtime ID {signalIndex}"); + + // Skip the sequence number and signal index when creating the buffer block measurement + BufferBlockMeasurement bufferBlockMeasurement = new(buffer, responseIndex + 8, responseLength - 8) + { + Metadata = measurementKey.Metadata + }; + + // Determine if this is the next buffer block in the sequence + if (sequenceNumber == m_expectedBufferBlockSequenceNumber) + { + List bufferBlockMeasurements = []; + int i; + + // Add the buffer block measurement to the list of measurements to be published + bufferBlockMeasurements.Add(bufferBlockMeasurement); + m_expectedBufferBlockSequenceNumber++; + + // Add cached buffer block measurements to the list of measurements to be published + for (i = 1; i < m_bufferBlockCache.Count; i++) + { + if (m_bufferBlockCache[i] is null) + break; + + bufferBlockMeasurements.Add(m_bufferBlockCache[i]!); + m_expectedBufferBlockSequenceNumber++; + } + + // Remove published measurements from the buffer block queue + if (m_bufferBlockCache.Count > 0) + m_bufferBlockCache.RemoveRange(0, i); + + // Publish measurements + OnNewMeasurements(bufferBlockMeasurements); + } + else + { + // Ensure that the list has at least as many + // elements as it needs to cache this measurement + for (int i = m_bufferBlockCache.Count; i <= bufferCacheIndex; i++) + m_bufferBlockCache.Add(null); + + // Insert this buffer block into the proper location in the list + m_bufferBlockCache[bufferCacheIndex] = bufferBlockMeasurement; + } + } + + LifetimeMeasurements += 1; + UpdateMeasurementsPerSecond(DateTime.UtcNow, 1); + break; + } + case ServerResponse.DataStartTime: + // Raise data start time event + OnDataStartTime(BigEndian.ToInt64(buffer, responseIndex)); + break; + case ServerResponse.ProcessingComplete: + // Raise input processing completed event + OnProcessingComplete(InterpretResponseMessage(buffer, responseIndex, responseLength)); + break; + case ServerResponse.UpdateSignalIndexCache: + { + int version = Version; + int cacheIndex = 0; + + // Get active cache index + if (version > 1) + cacheIndex = buffer[responseIndex++]; + + // Deserialize new signal index cache + SignalIndexCache remoteSignalIndexCache = DeserializeSignalIndexCache(buffer.BlockCopy(responseIndex, responseLength)); + SignalIndexCache signalIndexCache = new(DataSource, remoteSignalIndexCache); + + lock (m_signalIndexCacheLock) + { + Interlocked.CompareExchange(ref m_signalIndexCache, new SignalIndexCache[version > 1 ? 2 : 1], null); + + m_signalIndexCache[cacheIndex] = signalIndexCache; + m_remoteSignalIndexCache = remoteSignalIndexCache; + m_cacheIndex = cacheIndex; + } + + if (version > 1) + SendServerCommand(ServerCommand.ConfirmSignalIndexCache); + + FixExpectedMeasurementCounts(); + break; + } + case ServerResponse.UpdateBaseTimes: + // Get active time index + m_timeIndex = BigEndian.ToInt32(buffer, responseIndex); + responseIndex += 4; + + // Deserialize new base time offsets + m_baseTimeOffsets = [BigEndian.ToInt64(buffer, responseIndex), BigEndian.ToInt64(buffer, responseIndex + 8)]; + break; + case ServerResponse.UpdateCipherKeys: + // Move past active cipher index (not currently used anywhere else) + responseIndex++; + + // Extract remaining response + byte[] bytes = buffer.BlockCopy(responseIndex, responseLength - 1); + + // Deserialize new cipher keys + keyIVs = new byte[2][][]; + keyIVs[EvenKey] = new byte[2][]; + keyIVs[OddKey] = new byte[2][]; + + int index = 0; + + // Read even key size + int bufferLen = BigEndian.ToInt32(bytes, index); + index = 4; + + // Read even key + keyIVs[EvenKey][KeyIndex] = new byte[bufferLen]; + Buffer.BlockCopy(bytes, index, keyIVs[EvenKey][KeyIndex], 0, bufferLen); + index += bufferLen; + + // Read even initialization vector size + bufferLen = BigEndian.ToInt32(bytes, index); + index += 4; + + // Read even initialization vector + keyIVs[EvenKey][IVIndex] = new byte[bufferLen]; + Buffer.BlockCopy(bytes, index, keyIVs[EvenKey][IVIndex], 0, bufferLen); + index += bufferLen; + + // Read odd key size + bufferLen = BigEndian.ToInt32(bytes, index); + index += 4; + + // Read odd key + keyIVs[OddKey][KeyIndex] = new byte[bufferLen]; + Buffer.BlockCopy(bytes, index, keyIVs[OddKey][KeyIndex], 0, bufferLen); + index += bufferLen; + + // Read odd initialization vector size + bufferLen = BigEndian.ToInt32(bytes, index); + index += 4; + + // Read odd initialization vector + keyIVs[OddKey][IVIndex] = new byte[bufferLen]; + Buffer.BlockCopy(bytes, index, keyIVs[OddKey][IVIndex], 0, bufferLen); + //index += bufferLen; + + // Exchange keys + m_keyIVs = keyIVs; + + OnStatusMessage(MessageLevel.Info, "Successfully established new cipher keys for data packet transmissions."); + break; + case ServerResponse.Notify: + // Skip the 4-byte hash + string message = Encoding.GetString(buffer, responseIndex + 4, responseLength - 4); + + // Display notification + OnStatusMessage(MessageLevel.Info, $"NOTIFICATION: {message}"); + OnNotificationReceived(message); + + // Send confirmation of receipt of the notification + SendServerCommand(ServerCommand.ConfirmNotification, buffer.BlockCopy(responseIndex, 4)); + break; + case ServerResponse.ConfigurationChanged: + OnStatusMessage(MessageLevel.Info, "Received notification from publisher that configuration has changed."); + OnServerConfigurationChanged(); + + // Initiate meta-data refresh when publisher configuration has changed - we only do this + // for automatic connections since API style connections have to manually initiate a + // meta-data refresh. API style connection should attach to server configuration changed + // event and request meta-data refresh to complete automated cycle. + if (AutoConnect && AutoSynchronizeMetadata) + SendServerCommand(ServerCommand.MetaDataRefresh, MetadataFilters); + break; + } + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to process publisher response packet due to exception: {ex.Message}", ex)); + } + } + } + + private void ParseTSSCMeasurements(byte[] buffer, int packetLength, SignalIndexCache signalIndexCache, ref int responseIndex, List measurements) + { + TsscDecoder? decoder = signalIndexCache.TsscDecoder; + bool newDecoder = false; + + // Use TSSC compression to decompress measurements + if (decoder is null) + { + decoder = signalIndexCache.TsscDecoder = new TsscDecoder(); + decoder.SequenceNumber = 0; + newDecoder = true; + } + + if (buffer[responseIndex] != 85) + throw new Exception($"TSSC version not recognized: {buffer[responseIndex]}"); + + responseIndex++; + + int sequenceNumber = BigEndian.ToUInt16(buffer, responseIndex); + responseIndex += 2; + + if (sequenceNumber == 0) + { + if (!newDecoder) + { + if (decoder.SequenceNumber > 0) + OnStatusMessage(MessageLevel.Info, $"TSSC algorithm reset before sequence number: {decoder.SequenceNumber}", "TSSC"); + + decoder = signalIndexCache.TsscDecoder = new TsscDecoder(); + decoder.SequenceNumber = 0; + } + } + + if (decoder.SequenceNumber != sequenceNumber) + { + OnProcessException(MessageLevel.Warning, new InvalidDataException($"TSSC is out of sequence. Expecting: {decoder.SequenceNumber}, Received: {sequenceNumber} -- resetting connection.")); + Start(); + return; + } + + decoder.SetBuffer(buffer, responseIndex, packetLength - 3); + + while (decoder.TryGetMeasurement(out int id, out long time, out uint quality, out float value)) + { + if (!signalIndexCache.Reference.TryGetValue(id, out MeasurementKey? key) /* || key is null */) + continue; + + Measurement measurement = new() + { + Metadata = key.Metadata, + Timestamp = time, + StateFlags = (MeasurementStateFlags)quality, + Value = value + }; + + measurements.Add(measurement); + } + + decoder.SequenceNumber++; + + // Do not increment to 0 on roll-over + if (decoder.SequenceNumber == 0) + decoder.SequenceNumber = 1; + } + + private static bool IsUserCommand(ServerCommand command) + { + return s_userCommands.Contains(command); + } + + // Handles auto-connection subscription initialization + private void StartSubscription() + { + SubscribeToOutputMeasurements(!AutoSynchronizeMetadata); + + // Initiate meta-data refresh + if (AutoSynchronizeMetadata && !this.TemporalConstraintIsDefined()) + SendServerCommand(ServerCommand.MetaDataRefresh, MetadataFilters); + } + + private bool SubscribeToOutputMeasurements(bool metaDataRefreshCompleted) + { + StringBuilder filterExpression = new(); + string? dataChannel = null; + string? startTimeConstraint = null; + string? stopTimeConstraint = null; + int processingInterval = -1; + + // If TCP command channel is defined separately, then base connection string defines data channel + if (Settings.ContainsKey("commandChannel")) + dataChannel = ConnectionString; + + if (this.TemporalConstraintIsDefined()) + { + startTimeConstraint = StartTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fffffff"); + stopTimeConstraint = StopTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fffffff"); + processingInterval = ProcessingInterval; + } + + MeasurementKey[]? outputMeasurementKeys = AutoStart + ? this.OutputMeasurementKeys() + : RequestedOutputMeasurementKeys; + + if (outputMeasurementKeys is not null && outputMeasurementKeys.Length > 0) + { + // TODO: Handle "continued" subscribe operations so connection string size can be fixed + foreach (MeasurementKey measurementKey in outputMeasurementKeys) + { + if (filterExpression.Length > 0) + filterExpression.Append(';'); + + // Subscribe by associated Guid... + filterExpression.Append(measurementKey.SignalID); + } + + // Start unsynchronized subscription + #pragma warning disable 618 + return Subscribe(true, Throttled, filterExpression.ToString(), dataChannel, startTime: startTimeConstraint, stopTime: stopTimeConstraint, processingInterval: processingInterval, publishInterval: PublishInterval); + #pragma warning restore 618 + } + + Unsubscribe(); + + if (AutoStart && metaDataRefreshCompleted) + OnStatusMessage(MessageLevel.Error, "No measurements are currently defined for subscription."); + + return false; + } + + /// + /// Handles meta-data synchronization to local system. + /// + /// + /// This function should only be initiated from call to to make + /// sure only one meta-data synchronization happens at once. Users can override this method to customize + /// process of meta-data synchronization. + /// + protected virtual void SynchronizeMetadata() + { + bool dataMonitoringEnabled = false; + + // TODO: This function is complex and very closely tied to the current time-series data schema - perhaps it should be moved outside this class and referenced + // TODO: as a delegate that can be assigned and called to allow other schemas as well. DataPublisher is already very flexible in what data it can deliver. + try + { + DataSet? metadata = m_receivedMetadata; + + // Only perform database synchronization if meta-data has changed since last update + if (!SynchronizedMetadataChanged(metadata)) + return; + + if (metadata is null) + { + OnStatusMessage(MessageLevel.Error, "Meta-data synchronization was not performed, deserialized dataset was empty."); + return; + } + + // Reset data stream monitor while meta-data synchronization is in progress + if (m_dataStreamMonitor?.Enabled ?? false) + { + m_dataStreamMonitor.Enabled = false; + dataMonitoringEnabled = true; + } + + // Track total meta-data synchronization process time + Ticks startTime = DateTime.UtcNow.Ticks; + DateTime latestUpdateTime = DateTime.MinValue; + + // Open the configuration database using settings found in the config file + #if NET + using (AdoDataConnection database = new(ConfigSettings.Default)) + using (DbCommand command = database.Connection.CreateCommand()) + { + DbTransaction? transaction = null; + #else + using (AdoDataConnection database = new("systemSettings")) + using (IDbCommand command = database.Connection.CreateCommand()) + { + IDbTransaction? transaction = null; + #endif + if (UseTransactionForMetadata) + transaction = database.Connection.BeginTransaction(database.DefaultIsolationLevel); + + try + { + if (transaction is not null) + command.Transaction = transaction; + + // Query the actual record ID based on the known run-time ID for this subscriber device + object? sourceID = ExecuteScalar(command, $"SELECT SourceID FROM Runtime WHERE ID = {ID} AND SourceTable='Device'"); + + if (sourceID is null || sourceID == DBNull.Value) + return; + + int parentID = Convert.ToInt32(sourceID); + + // Validate that the subscriber device is marked as a concentrator (we are about to associate children devices with it) + if (!(ExecuteScalar(command, $"SELECT IsConcentrator FROM Device WHERE ID = {parentID}")?.ToString() ?? "false").ParseBoolean()) + ExecuteNonQuery(command, $"UPDATE Device SET IsConcentrator = 1 WHERE ID = {parentID}"); + + // Get any historian associated with the subscriber device + object? historianID = ExecuteScalar(command, $"SELECT HistorianID FROM Device WHERE ID = {parentID}"); + + #if !NET + // Determine the active node ID - we cache this since this value won't change for the lifetime of this class + if (m_nodeID == Guid.Empty) + m_nodeID = Guid.Parse(ExecuteScalar(command, $"SELECT NodeID FROM IaonInputAdapter WHERE ID = {(int)ID}")?.ToString() ?? Guid.Empty.ToString()); + + // Determine the protocol record auto-inc ID value for STTP - this value is also cached since it shouldn't change for the lifetime of this class + if (m_sttpProtocolID == 0) + m_sttpProtocolID = int.Parse(ExecuteScalar(command, "SELECT ID FROM Protocol WHERE Acronym='STTP'")?.ToString() ?? "0"); + #endif + + // Ascertain total number of actions required for all meta-data synchronization so some level feed back can be provided on progress + InitSyncProgress(metadata.Tables.Cast().Select(dataTable => (long)dataTable.Rows.Count).Sum() + 3); + + // Prefix all children devices with the name of the parent since the same device names could appear in different connections (helps keep device names unique) + string sourcePrefix = UseSourcePrefixNames ? $"{Name}!" : ""; + Dictionary deviceIDs = new(StringComparer.OrdinalIgnoreCase); + DateTime updateTime; + string deviceAcronym; + int deviceID; + + // Check to see if data for the "DeviceDetail" table was included in the meta-data + if (metadata.Tables.Contains("DeviceDetail")) + { + DataTable deviceDetail = metadata.Tables["DeviceDetail"]!; + DataRow[] deviceRows; + + // Define SQL statement to query if this device is already defined (this should always be based on the unique guid-based device ID) + string deviceExistsSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Device WHERE UniqueID = {0}", "uniqueID"); + + #if NET + // 0 1 2 3 4 5 6 7 8 9 -- 10 -- + // Define SQL statement to insert new device record + string insertDeviceSql = database.ParameterizedQueryString("INSERT INTO Device(ParentID, HistorianID, Acronym, Name, OriginalSource, AccessID, Longitude, Latitude, ContactList, ConnectionString, IsConcentrator, Internal, Enabled) " + + "VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, 0, {10}, " + (SyncIndependentDevices ? AutoEnableIndependentlySyncedDevices ? "1" : "0" : "1") + ")", + "parentID", "historianID", "acronym", "name", "originalSource", "accessID", "longitude", "latitude", "contactList", "connectionString", "internal"); + + // Define SQL statement to update existing device record + string updateDeviceSql = database.ParameterizedQueryString("UPDATE Device SET Acronym = {0}, Name = {1}, OriginalSource = {2}, HistorianID = {3}, AccessID = {4}, Longitude = {5}, Latitude = {6}, ContactList = {7}, Internal = {8} WHERE UniqueID = {9}", + "acronym", "name", "originalSource", "historianID", "accessID", "longitude", "latitude", "contactList", "internal", "uniqueID"); + + string updateDeviceWithConnectionStringSql = database.ParameterizedQueryString("UPDATE Device SET Acronym = {0}, Name = {1}, OriginalSource = {2}, HistorianID = {3}, AccessID = {4}, Longitude = {5}, Latitude = {6}, ContactList = {7}, ConnectionString = {8}, Internal = {9} WHERE UniqueID = {10}", + "acronym", "name", "originalSource", "historianID", "accessID", "longitude", "latitude", "contactList", "connectionString", "internal", "uniqueID"); + #else + // Define SQL statement to insert new device record + string insertDeviceSql = database.ParameterizedQueryString("INSERT INTO Device(NodeID, ParentID, HistorianID, Acronym, Name, ProtocolID, FramesPerSecond, OriginalSource, AccessID, Longitude, Latitude, ContactList, ConnectionString, IsConcentrator, Enabled) " + + "VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, 0, " + (SyncIndependentDevices ? AutoEnableIndependentlySyncedDevices ? "1" : "0" : "1") + ")", + "nodeID", "parentID", "historianID", "acronym", "name", "protocolID", "framesPerSecond", "originalSource", "accessID", "longitude", "latitude", "contactList", "connectionString"); + + // Define SQL statement to update existing device record + string updateDeviceSql = database.ParameterizedQueryString("UPDATE Device SET Acronym = {0}, Name = {1}, OriginalSource = {2}, ProtocolID = {3}, FramesPerSecond = {4}, HistorianID = {5}, AccessID = {6}, Longitude = {7}, Latitude = {8}, ContactList = {9} WHERE UniqueID = {10}", + "acronym", "name", "originalSource", "protocolID", "framesPerSecond", "historianID", "accessID", "longitude", "latitude", "contactList", "uniqueID"); + + string updateDeviceWithConnectionStringSql = database.ParameterizedQueryString("UPDATE Device SET Acronym = {0}, Name = {1}, OriginalSource = {2}, ProtocolID = {3}, FramesPerSecond = {4}, HistorianID = {5}, AccessID = {6}, Longitude = {7}, Latitude = {8}, ContactList = {9}, ConnectionString = {10} WHERE UniqueID = {11}", + "acronym", "name", "originalSource", "protocolID", "framesPerSecond", "historianID", "accessID", "longitude", "latitude", "contactList", "connectionString", "uniqueID"); + #endif + + // Define SQL statement to update device's guid-based unique ID after insert + string updateDeviceUniqueIDSql = database.ParameterizedQueryString("UPDATE Device SET UniqueID = {0} WHERE Acronym = {1}", "uniqueID", "acronym"); + + // Define SQL statement to query if a device can be safely updated + string deviceParentRestriction = SyncIndependentDevices ? "OriginalSource <> {1}" : "(ParentID <> {1} OR ParentID IS NULL)"; + string deviceIsUpdateableSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Device WHERE UniqueID = {0} AND " + deviceParentRestriction, "uniqueID", "parentID"); + + // Define SQL statement to retrieve device's auto-inc ID based on its unique guid-based ID + string queryDeviceIDSql = database.ParameterizedQueryString("SELECT ID FROM Device WHERE UniqueID = {0}", "uniqueID"); + + // Define SQL statement to retrieve all unique device ID's for the current parent to check for mismatches + string queryUniqueDeviceIDsSql = database.ParameterizedQueryString($"SELECT UniqueID FROM Device WHERE {(SyncIndependentDevices ? "OriginalSource" : "ParentID")} = {{0}}", "parentID"); + + // Define SQL statement to remove device records that no longer exist in the meta-data + string deleteDeviceSql = database.ParameterizedQueryString("DELETE FROM Device WHERE UniqueID = {0}", "uniqueID"); + + // Determine which device rows should be synchronized based on operational mode flags + if (ReceiveInternalMetadata && ReceiveExternalMetadata || MutualSubscription) + deviceRows = deviceDetail.Select(); + else if (ReceiveInternalMetadata) + deviceRows = deviceDetail.Select("OriginalSource IS NULL"); + else if (ReceiveExternalMetadata) + deviceRows = deviceDetail.Select("OriginalSource IS NOT NULL"); + else + deviceRows = []; + + // Check existence of optional meta-data fields + DataColumnCollection deviceDetailColumns = deviceDetail.Columns; + bool accessIDFieldExists = deviceDetailColumns.Contains("AccessID"); + bool longitudeFieldExists = deviceDetailColumns.Contains("Longitude"); + bool latitudeFieldExists = deviceDetailColumns.Contains("Latitude"); + bool companyAcronymFieldExists = deviceDetailColumns.Contains("CompanyAcronym"); + bool protocolNameFieldExists = deviceDetailColumns.Contains("ProtocolName"); + bool vendorAcronymFieldExists = deviceDetailColumns.Contains("VendorAcronym"); + bool vendorDeviceNameFieldExists = deviceDetailColumns.Contains("VendorDeviceName"); + bool interconnectionNameFieldExists = deviceDetailColumns.Contains("InterconnectionName"); + bool updatedOnFieldExists = deviceDetailColumns.Contains("UpdatedOn"); + bool connectionStringFieldExists = deviceDetailColumns.Contains("ConnectionString"); + bool framesPerSecondFieldExists = deviceDetailColumns.Contains("FramesPerSecond"); + object parentIDValue = SyncIndependentDevices ? parentID.ToString() : parentID; + int accessID = 0; + + List uniqueIDs = deviceRows + .Select(deviceRow => deviceRow.ConvertGuidField("UniqueID")) + .ToList(); + + // Remove any device records associated with this subscriber that no longer exist in the meta-data + if (uniqueIDs.Count > 0) + { + // ReSharper disable once AccessToDisposedClosure + IEnumerable retiredUniqueIDs = + RetrieveData(database, command, queryUniqueDeviceIDsSql, parentIDValue) + .Select() + .Select(deviceRow => deviceRow.ConvertGuidField("UniqueID")) + .Except(uniqueIDs); + + foreach (Guid retiredUniqueID in retiredUniqueIDs) + ExecuteNonQuery(command, deleteDeviceSql, database.Guid(retiredUniqueID)); + + UpdateSyncProgress(); + } + + foreach (DataRow row in deviceRows) + { + Guid uniqueID = row.ConvertGuidField("UniqueID"); + bool recordNeedsUpdating; + + // Determine if record has changed since last synchronization + if (updatedOnFieldExists) + { + try + { + updateTime = Convert.ToDateTime(row["UpdatedOn"]); + recordNeedsUpdating = updateTime > m_lastMetaDataRefreshTime; + + if (updateTime > latestUpdateTime) + latestUpdateTime = updateTime; + } + catch + { + recordNeedsUpdating = true; + } + } + else + { + recordNeedsUpdating = true; + } + + // We will synchronize meta-data only if the source owns this device, and it's not defined as a concentrator (these should normally be filtered by publisher - but we check just in case). + if (!row["IsConcentrator"].ToNonNullString("0").ParseBoolean()) + { + if (accessIDFieldExists) + accessID = row.ConvertField("AccessID"); + + // Get longitude and latitude values if they are defined + decimal longitude = 0M; + decimal latitude = 0M; + decimal? location; + string protocolName = string.Empty; + string connectionString = string.Empty; + + if (longitudeFieldExists) + { + location = row.ConvertNullableField("Longitude"); + + if (location.HasValue) + longitude = location.Value; + } + + if (latitudeFieldExists) + { + location = row.ConvertNullableField("Latitude"); + + if (location.HasValue) + latitude = location.Value; + } + + if (protocolNameFieldExists) + protocolName = row.Field("ProtocolName") ?? string.Empty; + + if (connectionStringFieldExists) + connectionString = row.Field("ConnectionString") ?? string.Empty; + + // Save any reported extraneous values from device meta-data in connection string formatted contact list - all fields are considered optional + Dictionary contactList = new(); + + if (companyAcronymFieldExists) + contactList["companyAcronym"] = row.Field("CompanyAcronym") ?? string.Empty; + + if (protocolNameFieldExists) + contactList["protocolName"] = protocolName; + + if (vendorAcronymFieldExists) + contactList["vendorAcronym"] = row.Field("VendorAcronym") ?? string.Empty; + + if (vendorDeviceNameFieldExists) + contactList["vendorDeviceName"] = row.Field("VendorDeviceName") ?? string.Empty; + + if (interconnectionNameFieldExists) + contactList["interconnectionName"] = row.Field("InterconnectionName") ?? string.Empty; + + #if !NET + int protocolID = m_sttpProtocolID; + #endif + + // If we are synchronizing independent devices, we need to determine the protocol ID for the device + // based on the protocol name defined in the meta-data + if (SyncIndependentDevices && !string.IsNullOrWhiteSpace(protocolName)) + { + #if NET + Dictionary settings = connectionString.ParseKeyValuePairs(); + settings["phasorProtocol"] = protocolName; + connectionString = settings.JoinKeyValuePairs(); + #else + string queryProtocolIDSql = database.ParameterizedQueryString("SELECT ID FROM Protocol WHERE Name = {0}", "protocolName"); + object? protocolIDValue = ExecuteScalar(command, queryProtocolIDSql, protocolName); + + if (protocolIDValue is not null && protocolIDValue is not DBNull) + protocolID = Convert.ToInt32(protocolIDValue); + + if (protocolID == 0) + protocolID = m_sttpProtocolID; + #endif + } + + // For mutual subscriptions where this subscription is owner (i.e., internal is true), we only sync devices that we did not provide + if (!MutualSubscription || !Internal || string.IsNullOrEmpty(row.Field("OriginalSource"))) + { + // Gateway is assuming ownership of the device records when the "internal" flag is true - this means the device's measurements can be forwarded to another party. From a device record perspective, + // ownership is inferred by setting 'OriginalSource' to null. When gateway doesn't own device records (i.e., the "internal" flag is false), this means the device's measurements can only be consumed + // locally - from a device record perspective this means the 'OriginalSource' field is set to the acronym of the PDC or PMU that generated the source measurements. This field allows a mirrored source + // restriction to be implemented later to ensure all devices in an output protocol came from the same original source connection, if desired. + object originalSource = SyncIndependentDevices ? parentID.ToString() : + Internal ? DBNull.Value : + string.IsNullOrEmpty(row.Field("ParentAcronym")) ? sourcePrefix + row.Field("Acronym") : + sourcePrefix + row.Field("ParentAcronym"); + + // Determine if device record already exists + if (Convert.ToInt32(ExecuteScalar(command, deviceExistsSql, database.Guid(uniqueID))) == 0) + { + #if NET + // Insert new device record + ExecuteNonQuery(command, insertDeviceSql, SyncIndependentDevices ? DBNull.Value : parentID, + historianID, sourcePrefix + row.Field("Acronym"), row.Field("Name"), originalSource, + accessID, longitude, latitude, contactList.JoinKeyValuePairs(), connectionString, database.Bool(Internal)); + #else + // Insert new device record + ExecuteNonQuery(command, insertDeviceSql, database.Guid(m_nodeID), SyncIndependentDevices ? DBNull.Value : parentID, + historianID, sourcePrefix + row.Field("Acronym"), row.Field("Name"), protocolID, + framesPerSecondFieldExists ? row.ConvertField("FramesPerSecond") : 30, originalSource, accessID, + longitude, latitude, contactList.JoinKeyValuePairs(), connectionString); + #endif + + // Guids are normally auto-generated during insert - after insertion update the Guid so that it matches the source data. Most of the database + // scripts have triggers that support properly assigning the Guid during an insert, but this code ensures the Guid will always get assigned. + ExecuteNonQuery(command, updateDeviceUniqueIDSql, database.Guid(uniqueID), sourcePrefix + row.Field("Acronym")); + } + else if (recordNeedsUpdating) + { + // Perform safety check to preserve device records which are not safe to overwrite (e.g., device already exists locally as part of another connection) + if (Convert.ToInt32(ExecuteScalar(command, deviceIsUpdateableSql, database.Guid(uniqueID), parentIDValue)) > 0) + continue; + + #if NET + // Update existing device record + if (connectionStringFieldExists) + ExecuteNonQuery(command, updateDeviceWithConnectionStringSql, sourcePrefix + row.Field("Acronym"), row.Field("Name"), + originalSource, historianID, accessID, longitude, latitude, contactList.JoinKeyValuePairs(), connectionString, database.Bool(Internal), database.Guid(uniqueID)); + else + ExecuteNonQuery(command, updateDeviceSql, sourcePrefix + row.Field("Acronym"), row.Field("Name"), + originalSource, historianID, accessID, longitude, latitude, contactList.JoinKeyValuePairs(), database.Bool(Internal), database.Guid(uniqueID)); + #else + // Update existing device record + if (connectionStringFieldExists) + ExecuteNonQuery(command, updateDeviceWithConnectionStringSql, sourcePrefix + row.Field("Acronym"), row.Field("Name"), + originalSource, protocolID, framesPerSecondFieldExists ? row.ConvertField("FramesPerSecond") : 30, historianID, accessID, longitude, latitude, contactList.JoinKeyValuePairs(), connectionString, database.Guid(uniqueID)); + else + ExecuteNonQuery(command, updateDeviceSql, sourcePrefix + row.Field("Acronym"), row.Field("Name"), + originalSource, protocolID, framesPerSecondFieldExists ? row.ConvertField("FramesPerSecond") : 30, historianID, accessID, longitude, latitude, contactList.JoinKeyValuePairs(), database.Guid(uniqueID)); + #endif + } + } + } + + // Capture local device ID auto-inc value for measurement association + deviceIDs[row.Field("Acronym")!] = Convert.ToInt32(ExecuteScalar(command, queryDeviceIDSql, database.Guid(uniqueID))); + + // Periodically notify user about synchronization progress + UpdateSyncProgress(); + } + } + + // Check to see if data for the "MeasurementDetail" table was included in the meta-data + if (metadata.Tables.Contains("MeasurementDetail")) + { + DataTable measurementDetail = metadata.Tables["MeasurementDetail"]!; + List signalIDs = []; + DataRow[] measurementRows; + + // Define SQL statement to query if this measurement is already defined (this should always be based on the unique signal ID Guid) + string measurementExistsSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Measurement WHERE SignalID = {0}", "signalID"); + + // Define SQL statement to query if this measurement is already defined (this will be used before identity insert) + string identityMeasurementExistsSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Measurement WHERE PointID = {0}", "pointID"); + + // Define SQL statement to insert new measurement record + string insertMeasurementSql = database.ParameterizedQueryString("INSERT INTO Measurement(DeviceID, HistorianID, PointTag, AlternateTag, SignalTypeID, PhasorSourceIndex, SignalReference, Description, Internal, Subscribed, Enabled) " + + "VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, 0, 1)", "deviceID", "historianID", "pointTag", "tempAlternateTagID", "signalTypeID", "phasorSourceIndex", "signalReference", "description", "internal"); + + // Define SQL statement to insert new measurement record + string identityInsertMeasurementSql = database.ParameterizedQueryString("INSERT INTO Measurement(PointID, DeviceID, HistorianID, PointTag, AlternateTag, SignalTypeID, PhasorSourceIndex, SignalReference, Description, Internal, Subscribed, Enabled) " + + "VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, 0, 1)", "pointID", "deviceID", "historianID", "pointTag", "tempAlternateTagID", "signalTypeID", "phasorSourceIndex", "signalReference", "description", "internal"); + + // Define SQL statement to update measurement's signal ID after insert, restoring original signal ID and alternate tag from meta-data + string updateMeasurementSignalIDSql = database.ParameterizedQueryString("UPDATE Measurement SET SignalID = {0}, AlternateTag = {1} WHERE AlternateTag = {2}", "signalID", "alternateTag", "tempAlternateTagID"); + + // Define SQL statement to update existing measurement record + string updateMeasurementSql = database.ParameterizedQueryString("UPDATE Measurement SET HistorianID = {0}, PointTag = {1}, AlternateTag = {2}, SignalTypeID = {3}, PhasorSourceIndex = {4}, SignalReference = {5}, Description = {6}, Internal = {7} WHERE SignalID = {8}", + "historianID", "pointTag", "alternateTag", "signalTypeID", "phasorSourceIndex", "signalReference", "description", "internal", "signalID"); + + // Define SQL statement to update existing measurement record + string identityUpdateMeasurementSql = database.ParameterizedQueryString("UPDATE Measurement SET DeviceID = {0}, HistorianID = {1}, PointTag = {2}, AlternateTag = {3}, SignalTypeID = {4}, PhasorSourceIndex = {5}, SignalReference = {6}, Description = {7}, Internal = {8}, Subscribed = 0, Enabled = 1, SignalID = {9} WHERE PointID = {10}", + "deviceID", "historianID", "pointTag", "tempAlternateTagID", "signalTypeID", "phasorSourceIndex", "signalReference", "description", "internal", "signalID", "pointID"); + + // Define SQL statement to retrieve all measurement signal ID's for the current parent to check for mismatches - note that we use the ActiveMeasurements view + // since it associates measurements with their top-most parent runtime device ID, this allows us to easily query all measurements for the parent device + string queryMeasurementSignalIDsSql = database.ParameterizedQueryString("SELECT SignalID FROM ActiveMeasurement WHERE DeviceID = {0}", "deviceID"); + + // Define SQL statement to retrieve measurement's associated device ID, i.e., actual record ID, based on measurement's signal ID + string queryMeasurementDeviceIDSql = database.ParameterizedQueryString("SELECT DeviceID FROM Measurement WHERE SignalID = {0}", "signalID"); + + // Load signal type ID's from local database associated with their acronym for proper signal type translation + Dictionary signalTypeIDs = new(StringComparer.OrdinalIgnoreCase); + + string? signalTypeAcronym; + + foreach (DataRow row in RetrieveData(database, command, "SELECT ID, Acronym FROM SignalType").Rows) + { + signalTypeAcronym = row.Field("Acronym"); + + if (!string.IsNullOrWhiteSpace(signalTypeAcronym)) + signalTypeIDs[signalTypeAcronym] = row.ConvertField("ID"); + } + + // Define local signal type ID deletion exclusion set + string deleteCondition = ""; + + if (MutualSubscription && !Internal) + { + // For mutual subscriptions where this subscription is renter (i.e., internal is false), do not delete measurements that are locally owned + deleteCondition = " AND Internal == 0"; + } + else + { + List excludedSignalTypeIDs = []; + + // We are intentionally ignoring CALC and ALRM signals during measurement deletion since if you have subscribed to a device and subsequently created local + // calculations and alarms associated with this device, these signals are locally owned and not part of the publisher subscription stream. As a result any + // CALC or ALRM measurements that are created at source and then removed could be orphaned in subscriber. The best fix would be to have a simple flag that + // clearly designates that a measurement was created locally and is not part of the remote synchronization set. + if (!AutoDeleteCalculatedMeasurements && signalTypeIDs.TryGetValue("CALC", out int signalTypeID)) + excludedSignalTypeIDs.Add(signalTypeID); + + if (!AutoDeleteAlarmMeasurements && signalTypeIDs.TryGetValue("ALRM", out signalTypeID)) + excludedSignalTypeIDs.Add(signalTypeID); + + if (excludedSignalTypeIDs.Count > 0) + deleteCondition = $" AND NOT SignalTypeID IN ({excludedSignalTypeIDs.ToDelimitedString(',')})"; + } + + // Define SQL statement to remove device records that no longer exist in the meta-data + string deleteMeasurementSql = database.ParameterizedQueryString($"DELETE FROM Measurement WHERE SignalID = {{0}}{deleteCondition}", "signalID"); + + // Determine which measurement rows should be synchronized based on operational mode flags + if (ReceiveInternalMetadata && ReceiveExternalMetadata) + measurementRows = measurementDetail.Select(); + else if (ReceiveInternalMetadata) + measurementRows = measurementDetail.Select("Internal <> 0"); + else if (ReceiveExternalMetadata) + measurementRows = measurementDetail.Select("Internal = 0"); + else + measurementRows = []; + + // Check existence of optional meta-data fields + DataColumnCollection measurementDetailColumns = measurementDetail.Columns; + bool phasorSourceIndexFieldExists = measurementDetailColumns.Contains("PhasorSourceIndex"); + bool updatedOnFieldExists = measurementDetailColumns.Contains("UpdatedOn"); + bool alternateTagFieldExists = measurementDetailColumns.Contains("AlternateTag"); + + object phasorSourceIndex = DBNull.Value; + object alternateTag = DBNull.Value; + + if (UseIdentityInsertsForMetadata && database.IsSQLServer) + ExecuteNonQuery(command, "SET IDENTITY_INSERT Measurement ON"); + + try + { + foreach (DataRow row in measurementRows) + { + bool recordNeedsUpdating; + + // Determine if record has changed since last synchronization + if (updatedOnFieldExists) + { + try + { + updateTime = Convert.ToDateTime(row["UpdatedOn"]); + recordNeedsUpdating = updateTime > m_lastMetaDataRefreshTime; + + if (updateTime > latestUpdateTime) + latestUpdateTime = updateTime; + } + catch + { + recordNeedsUpdating = true; + } + } + else + { + recordNeedsUpdating = true; + } + + // Get device and signal type acronyms + deviceAcronym = row.Field("DeviceAcronym") ?? string.Empty; + signalTypeAcronym = row.Field("SignalAcronym") ?? string.Empty; + + // Get phasor source index if field is defined + if (phasorSourceIndexFieldExists) + { + // Using ConvertNullableField extension since publisher could use SQLite database in which case + // all integers would arrive in data set as longs and need to be converted back to integers + int? index = row.ConvertNullableField("PhasorSourceIndex"); + phasorSourceIndex = index ?? (object)DBNull.Value; + } + + // Get alternate tag if field is defined + if (alternateTagFieldExists) + alternateTag = row.Field("AlternateTag") ?? (object)DBNull.Value; + + // Make sure we have an associated device and signal type already defined for the measurement + if (!string.IsNullOrWhiteSpace(deviceAcronym) && deviceIDs.ContainsKey(deviceAcronym) && !string.IsNullOrWhiteSpace(signalTypeAcronym) && signalTypeIDs.ContainsKey(signalTypeAcronym)) + { + Guid signalID = row.ConvertGuidField("SignalID"); + + // Track unique measurement signal Guids in this meta-data session, we'll need to remove any old associated measurements that no longer exist + signalIDs.Add(signalID); + + // Prefix the tag name with the "updated" device name + string pointTag = sourcePrefix + row.Field("PointTag"); + + // Look up associated device ID (local DB auto-inc) + deviceID = deviceIDs[deviceAcronym]; + + // Determine if measurement record already exists + if (Convert.ToInt32(ExecuteScalar(command, measurementExistsSql, database.Guid(signalID))) == 0) + { + string temporaryAlternateTagID = Guid.NewGuid().ToString(); + + // Insert new measurement record + if (UseIdentityInsertsForMetadata && MeasurementKey.TryParse(row.Field("ID")!, out MeasurementKey measurementKey)) + { + long pointID = (long)measurementKey.ID; + + if (Convert.ToInt32(ExecuteScalar(command, identityMeasurementExistsSql, pointID)) == 0) + ExecuteNonQuery(command, identityInsertMeasurementSql, pointID, deviceID, historianID, pointTag, temporaryAlternateTagID, signalTypeIDs[signalTypeAcronym], phasorSourceIndex, sourcePrefix + row.Field("SignalReference"), row.Field("Description") ?? string.Empty, database.Bool(Internal)); + else + ExecuteNonQuery(command, identityUpdateMeasurementSql, deviceID, historianID, pointTag, temporaryAlternateTagID, signalTypeIDs[signalTypeAcronym], phasorSourceIndex, sourcePrefix + row.Field("SignalReference"), row.Field("Description") ?? string.Empty, database.Bool(Internal), database.Guid(signalID), pointID); + } + else + { + ExecuteNonQuery(command, insertMeasurementSql, deviceID, historianID, pointTag, temporaryAlternateTagID, signalTypeIDs[signalTypeAcronym], phasorSourceIndex, sourcePrefix + row.Field("SignalReference"), row.Field("Description") ?? string.Empty, database.Bool(Internal)); + } + + // Guids are normally auto-generated during insert - after insertion update the Guid so that it matches the source data. Most of the database + // scripts have triggers that support properly assigning the Guid during an insert, but this code ensures the Guid will always get assigned. + // TODO: Ensure database schemas define an index on the AlternateTag field to optimize this update + ExecuteNonQuery(command, updateMeasurementSignalIDSql, database.Guid(signalID), alternateTag, temporaryAlternateTagID); + } + else if (recordNeedsUpdating) + { + // Update existing measurement record. Note that this update assumes that measurements will remain associated with a static source device. + ExecuteNonQuery(command, updateMeasurementSql, historianID, pointTag, alternateTag, signalTypeIDs[signalTypeAcronym], phasorSourceIndex, sourcePrefix + row.Field("SignalReference"), row.Field("Description") ?? string.Empty, database.Bool(Internal), database.Guid(signalID)); + } + } + + // Periodically notify user about synchronization progress + UpdateSyncProgress(); + } + } + finally + { + if (UseIdentityInsertsForMetadata && database.IsSQLServer) + ExecuteNonQuery(command, "SET IDENTITY_INSERT Measurement OFF"); + } + + // Remove any measurement records associated with existing devices in this session but no longer exist in the meta-data + if (signalIDs.Count > 0) + { + // Sort signal ID list so that binary search can be used for quick lookups + signalIDs.Sort(); + + // Query all the guid-based signal ID's for all measurement records associated with the parent device using run-time ID + DataTable measurementSignalIDs = RetrieveData(database, command, queryMeasurementSignalIDsSql, (int)ID); + + // Walk through each database record and see if the measurement exists in the provided meta-data + foreach (DataRow measurementRow in measurementSignalIDs.Rows) + { + Guid signalID = measurementRow.ConvertGuidField("SignalID"); + + // Remove any measurements in the database that are associated with received devices and do not exist in the meta-data + if (signalIDs.BinarySearch(signalID) >= 0) + continue; + + // Measurement was not in the meta-data, get the measurement's actual record based ID for its associated device + object? measurementDeviceID = ExecuteScalar(command, queryMeasurementDeviceIDSql, database.Guid(signalID)); + + // If the unknown measurement is directly associated with a device that exists in the meta-data it is assumed that this measurement + // was removed from the publishing system and no longer exists therefore we remove it from the local measurement cache. If the user + // needs custom local measurements associated with a remote device, they should be associated with the parent device only. + if (measurementDeviceID is not null && measurementDeviceID is not DBNull && deviceIDs.ContainsValue(Convert.ToInt32(measurementDeviceID))) + ExecuteNonQuery(command, deleteMeasurementSql, database.Guid(signalID)); + } + + UpdateSyncProgress(); + } + } + + // Check to see if data for the "PhasorDetail" table was included in the meta-data + if (metadata.Tables.Contains("PhasorDetail")) + { + #if NET + const string PrimaryVoltageID = "PrimaryVoltageID"; + const string DestinationPhasorID = "DestinationPhasorID"; + #else + const string PrimaryVoltageID = "DestinationPhasorID"; + const string DestinationPhasorID = "PrimaryVoltageID"; + #endif + + DataTable phasorDetail = metadata.Tables["PhasorDetail"]!; + Dictionary> definedSourceIndices = new(); + Dictionary metadataToDatabaseIDMap = new(); + Dictionary sourceToDestinationIDMap = new(); + + // Phasor data is normally only needed so that the user can properly generate a mirrored IEEE C37.118 output stream from the source data. + // This is necessary since, in this protocol, the phasors are described (i.e., labeled) as a unit (i.e., as a complex number) instead of + // as two distinct angle and magnitude measurements. + + // Define SQL statement to query if phasor record is already defined (no Guid is defined for these simple label records) + string phasorExistsSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Phasor WHERE DeviceID = {0} AND SourceIndex = {1}", "deviceID", "sourceIndex"); + + #if NET + // Define SQL statement to insert new phasor record + string insertPhasorSql = database.ParameterizedQueryString("INSERT INTO Phasor(DeviceID, Label, Type, Phase, SourceIndex, Internal) VALUES ({0}, {1}, {2}, {3}, {4}, {5})", "deviceID", "label", "type", "phase", "sourceIndex", "internal"); + + // Define SQL statement to update existing phasor record + string updatePhasorSql = database.ParameterizedQueryString("UPDATE Phasor SET Label = {0}, Type = {1}, Phase = {2}, Internal = {3} WHERE DeviceID = {4} AND SourceIndex = {5}", "label", "type", "phase", "internal", "deviceID", "sourceIndex"); + #else + // Define SQL statement to insert new phasor record + string insertPhasorSql = database.ParameterizedQueryString("INSERT INTO Phasor(DeviceID, Label, Type, Phase, SourceIndex) VALUES ({0}, {1}, {2}, {3}, {4})", "deviceID", "label", "type", "phase", "sourceIndex"); + + // Define SQL statement to update existing phasor record + string updatePhasorSql = database.ParameterizedQueryString("UPDATE Phasor SET Label = {0}, Type = {1}, Phase = {2} WHERE DeviceID = {3} AND SourceIndex = {4}", "label", "type", "phase", "deviceID", "sourceIndex"); + #endif + + // Define SQL statement to delete a phasor record + string deletePhasorSql = database.ParameterizedQueryString("DELETE FROM Phasor WHERE DeviceID = {0}", "deviceID"); + + // Define SQL statement to query phasor record ID + string queryPhasorIDSql = database.ParameterizedQueryString("SELECT ID FROM Phasor WHERE DeviceID = {0} AND SourceIndex = {1}", "deviceID", "sourceIndex"); + + // Define SQL statement to update destinationPhasorID field of existing phasor record + string updatePrimaryVoltageIDSql = database.ParameterizedQueryString($"UPDATE Phasor SET {PrimaryVoltageID} = {{0}} WHERE ID = {{1}}", "primaryVoltageID", "id"); + + // Define SQL statement to update phasor BaseKV + string updatePhasorBaseKVSql = database.ParameterizedQueryString("UPDATE Phasor SET BaseKV = {0} WHERE DeviceID = {1} AND SourceIndex = {2}", "baseKV", "deviceID", "sourceIndex"); + + // Check existence of optional meta-data fields + DataColumnCollection phasorDetailColumns = phasorDetail.Columns; + bool phasorIDFieldExists = phasorDetailColumns.Contains("ID"); + bool primaryVoltageIDFieldExists = phasorDetailColumns.Contains(PrimaryVoltageID) || phasorDetailColumns.Contains(DestinationPhasorID); + bool baseKVFieldExists = phasorDetailColumns.Contains("BaseKV"); + + foreach (DataRow row in phasorDetail.Rows) + { + // Get device acronym + deviceAcronym = row.Field("DeviceAcronym") ?? string.Empty; + + // Make sure we have an associated device already defined for the phasor record + // ReSharper disable once CanSimplifyDictionaryLookupWithTryGetValue + if (!string.IsNullOrWhiteSpace(deviceAcronym) && deviceIDs.ContainsKey(deviceAcronym)) + { + bool recordNeedsUpdating; + + // Determine if record has changed since last synchronization + try + { + updateTime = Convert.ToDateTime(row["UpdatedOn"]); + recordNeedsUpdating = updateTime > m_lastMetaDataRefreshTime; + + if (updateTime > latestUpdateTime) + latestUpdateTime = updateTime; + } + catch + { + recordNeedsUpdating = true; + } + + deviceID = deviceIDs[deviceAcronym]; + + int sourceIndex = row.ConvertField("SourceIndex"); + bool updateRecord = false; + + // Determine if phasor record already exists + if (Convert.ToInt32(ExecuteScalar(command, phasorExistsSql, deviceID, sourceIndex)) == 0) + { + #if NET + // Insert new phasor record + ExecuteNonQuery(command, insertPhasorSql, deviceID, row.Field("Label") ?? "undefined", (row.Field("Type") ?? "V").TruncateLeft(1), (row.Field("Phase") ?? "+").TruncateLeft(1), sourceIndex, database.Bool(Internal)); + #else + // Insert new phasor record + ExecuteNonQuery(command, insertPhasorSql, deviceID, row.Field("Label") ?? "undefined", (row.Field("Type") ?? "V").TruncateLeft(1), (row.Field("Phase") ?? "+").TruncateLeft(1), sourceIndex); + #endif + updateRecord = true; + } + else if (recordNeedsUpdating) + { + #if NET + // Update existing phasor record + ExecuteNonQuery(command, updatePhasorSql, row.Field("Label") ?? "undefined", (row.Field("Type") ?? "V").TruncateLeft(1), (row.Field("Phase") ?? "+").TruncateLeft(1), database.Bool(Internal), deviceID, sourceIndex); + #else + // Update existing phasor record + ExecuteNonQuery(command, updatePhasorSql, row.Field("Label") ?? "undefined", (row.Field("Type") ?? "V").TruncateLeft(1), (row.Field("Phase") ?? "+").TruncateLeft(1), deviceID, sourceIndex); + #endif + updateRecord = true; + } + + if (updateRecord && baseKVFieldExists) + ExecuteNonQuery(command, updatePhasorBaseKVSql, row.ConvertField("BaseKV"), deviceID, sourceIndex); + + if (phasorIDFieldExists && primaryVoltageIDFieldExists) + { + int sourcePhasorID = row.ConvertField("ID"); + + // Using ConvertNullableField extension since publisher could use SQLite database in which case + // all integers would arrive in data set as longs and need to be converted back to integers + int? destinationPhasorID = row.ConvertNullableField(phasorDetailColumns.Contains(PrimaryVoltageID) ? + PrimaryVoltageID : + DestinationPhasorID); + + if (destinationPhasorID.HasValue) + sourceToDestinationIDMap[sourcePhasorID] = destinationPhasorID.Value; + + // Map all metadata phasor IDs to associated local database phasor IDs + metadataToDatabaseIDMap[sourcePhasorID] = Convert.ToInt32(ExecuteScalar(command, queryPhasorIDSql, deviceID, sourceIndex)); + } + + // Track defined phasors for each device + definedSourceIndices.GetOrAdd(deviceID, _ => []).Add(sourceIndex); + } + + // Periodically notify user about synchronization progress + UpdateSyncProgress(); + } + + // Once all phasor records have been processed, handle updating of destination phasor IDs + foreach (KeyValuePair item in sourceToDestinationIDMap) + { + if (metadataToDatabaseIDMap.TryGetValue(item.Key, out int sourcePhasorID) && metadataToDatabaseIDMap.TryGetValue(item.Value, out int destinationPhasorID)) + ExecuteNonQuery(command, updatePrimaryVoltageIDSql, destinationPhasorID, sourcePhasorID); + } + + // For mutual subscriptions where this subscription is owner (i.e., internal is true), do not delete any phasor data - it will be managed by owner only + if (!MutualSubscription || !Internal) + { + // Remove any phasor records associated with existing devices in this session but no longer exist in the meta-data + foreach (int id in deviceIDs.Values) + { + string deleteSql = definedSourceIndices.TryGetValue(id, out List? sourceIndices) ? $"{deletePhasorSql} AND SourceIndex NOT IN ({string.Join(",", sourceIndices)})" : deletePhasorSql; + + ExecuteNonQuery(command, deleteSql, id); + } + } + } + + transaction?.Commit(); + + // Update local in-memory synchronized meta-data cache + m_synchronizedMetadata = metadata; + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to synchronize meta-data to local cache: {ex.Message}", ex)); + + if (transaction is not null) + { + try + { + transaction.Rollback(); + } + catch (Exception rollbackException) + { + OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to roll back database transaction due to exception: {rollbackException.Message}", rollbackException)); + } + } + + return; + } + finally + { + transaction?.Dispose(); + } + } + + // New signals may have been defined, take original remote signal index cache and apply changes + if (m_remoteSignalIndexCache is not null && m_signalIndexCache is not null) + { + SignalIndexCache? originalReference, remoteSignalIndexCache; + + lock (m_signalIndexCacheLock) + { + originalReference = m_signalIndexCache[m_cacheIndex]; + remoteSignalIndexCache = m_remoteSignalIndexCache; + } + + SignalIndexCache signalIndexCache = new(DataSource, remoteSignalIndexCache); + + if (signalIndexCache.Reference.Count > 0) + { + lock (m_signalIndexCacheLock) + { + if (ReferenceEquals(originalReference, m_signalIndexCache[m_cacheIndex])) + { + signalIndexCache.TsscDecoder = m_signalIndexCache[m_cacheIndex]!.TsscDecoder; + m_signalIndexCache[m_cacheIndex] = signalIndexCache; + } + } + } + } + + m_lastMetaDataRefreshTime = latestUpdateTime > DateTime.MinValue ? latestUpdateTime : DateTime.UtcNow; + + OnStatusMessage(MessageLevel.Info, $"Meta-data synchronization completed successfully in {(DateTime.UtcNow.Ticks - startTime).ToElapsedTimeString(2)}"); + + // Send notification that system configuration has changed + OnConfigurationChanged(); + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to synchronize meta-data to local cache: {ex.Message}", ex)); + } + finally + { + // Restart data stream monitor after meta-data synchronization if it was originally enabled + if (dataMonitoringEnabled && m_dataStreamMonitor is not null) + m_dataStreamMonitor.Enabled = true; + } + } + + // Since Gemstone data extensions moved position of timeout parameter to a more logical ordinal location so that it will not + // conflict with parameters, we establish overloads for .NET core / framework versions of needed data extension methods +#if NET + private DataTable RetrieveData(AdoDataConnection _, DbCommand command, string sql, params object?[] parameters) + { + return command.RetrieveData(MetadataSynchronizationTimeout, sql, parameters); + } + + private void ExecuteNonQuery(DbCommand command, string sql, params object?[] parameters) + { + command.ExecuteNonQuery(MetadataSynchronizationTimeout, sql, parameters); + } + + private object? ExecuteScalar(DbCommand command, string sql, params object?[] parameters) + { + return command.ExecuteScalar(MetadataSynchronizationTimeout, sql, parameters); + } +#else + private DataTable RetrieveData(AdoDataConnection database, IDbCommand command, string sql, params object?[] parameters) + { + return command.RetrieveData(database.AdapterType, sql, MetadataSynchronizationTimeout, parameters); + } + + private void ExecuteNonQuery(IDbCommand command, string sql, params object?[] parameters) + { + command.ExecuteNonQuery(sql, MetadataSynchronizationTimeout, parameters); + } + + private object? ExecuteScalar(IDbCommand command, string sql, params object?[] parameters) + { + return command.ExecuteScalar(sql, MetadataSynchronizationTimeout, parameters); + } +#endif + + private void InitSyncProgress(long totalActions) + { + m_syncProgressTotalActions = totalActions; + m_syncProgressActionsCount = 0; + m_syncProgressLastMessage = DateTime.UtcNow.Ticks; + } + + private void UpdateSyncProgress() + { + m_syncProgressActionsCount++; + + // We update user on progress every 15 seconds or at 100% complete + if (DateTime.UtcNow.Ticks - m_syncProgressLastMessage < 150000000 && m_syncProgressActionsCount < m_syncProgressTotalActions) + return; + + OnStatusMessage(MessageLevel.Info, $"Meta-data synchronization is {m_syncProgressActionsCount / (double)m_syncProgressTotalActions:0.0%} complete..."); + m_syncProgressLastMessage = DateTime.UtcNow.Ticks; + } + + private SignalIndexCache DeserializeSignalIndexCache(byte[] buffer) + { + CompressionModes compressionModes = (CompressionModes)(m_operationalModes & OperationalModes.CompressionModeMask); + bool compressSignalIndexCache = (m_operationalModes & OperationalModes.CompressSignalIndexCache) > 0; + GZipStream? inflater = null; + + if (compressSignalIndexCache && compressionModes.HasFlag(CompressionModes.GZip)) + { + try + { + using MemoryStream compressedData = new(buffer); + inflater = new GZipStream(compressedData, CompressionMode.Decompress, true); + buffer = inflater.ReadStream(); + } + finally + { + inflater?.Close(); + } + } + + SignalIndexCache deserializedCache = new() { Encoding = Encoding }; + deserializedCache.ParseBinaryImage(buffer, 0, buffer.Length); + + return deserializedCache; + } + + private DataSet DeserializeMetadata(byte[] buffer) + { + CompressionModes compressionModes = (CompressionModes)(m_operationalModes & OperationalModes.CompressionModeMask); + bool compressMetadata = (m_operationalModes & OperationalModes.CompressMetadata) > 0; + Ticks startTime = DateTime.UtcNow.Ticks; + DataSet deserializedMetadata; + GZipStream? inflater = null; + + if (compressMetadata && compressionModes.HasFlag(CompressionModes.GZip)) + { + try + { + // Insert compressed data into compressed buffer + using MemoryStream compressedData = new(buffer); + inflater = new GZipStream(compressedData, CompressionMode.Decompress, true); + buffer = inflater.ReadStream(); + } + finally + { + inflater?.Close(); + } + } + + // Copy decompressed data into encoded buffer + using (MemoryStream encodedData = new(buffer)) + using (XmlTextReader xmlReader = new(encodedData)) + { + // Read encoded data into data set as XML + deserializedMetadata = new DataSet(); + deserializedMetadata.ReadXml(xmlReader, XmlReadMode.ReadSchema); + } + + long rowCount = deserializedMetadata.Tables.Cast().Select(dataTable => (long)dataTable.Rows.Count).Sum(); + + if (rowCount > 0) + { + Time elapsedTime = (DateTime.UtcNow.Ticks - startTime).ToSeconds(); + OnStatusMessage(MessageLevel.Info, $"Received a total of {rowCount:N0} records spanning {deserializedMetadata.Tables.Count:N0} tables of meta-data that was {(compressMetadata ? "uncompressed and " : "")}deserialized in {elapsedTime.ToString(3)}..."); + } + + return deserializedMetadata; + } + + private static Encoding GetCharacterEncoding(OperationalEncoding operationalEncoding) + { + Encoding encoding = operationalEncoding switch + { + OperationalEncoding.UTF16LE => Encoding.Unicode, + OperationalEncoding.UTF16BE => Encoding.BigEndianUnicode, + OperationalEncoding.UTF8 => Encoding.UTF8, + _ => throw new InvalidOperationException($"Unsupported encoding detected: {operationalEncoding}") + }; + + return encoding; + } + + // Socket exception handler + private bool HandleSocketException(Exception? ex) + { + // WSAECONNABORTED and WSAECONNRESET are common errors after a client disconnect, + // if they happen for other reasons, make sure disconnect procedure is handled + if (ex is SocketException { ErrorCode: 10053 or 10054 }) + { + DisconnectClient(); + return true; + } + + if (ex is not null) + HandleSocketException(ex.InnerException); + + return false; + } + + // Disconnect client, restarting if disconnect was not intentional + private void DisconnectClient() + { + // Mark end of any data transmission in run-time log + if (m_runTimeLog is not null && m_runTimeLog.Enabled) + { + m_runTimeLog.StopTime = DateTimeOffset.UtcNow; + m_runTimeLog.Enabled = false; + } + + // Stop data gap recovery operations + if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null) + { + try + { + m_dataGapRecoverer.Enabled = false; + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Exception while attempting to flush data gap recoverer log: {ex.Message}", ex)); + } + } + + DataChannel = null; + m_metadataRefreshPending = false; + + if (m_serverCommandChannel is null) + { + // If user didn't initiate disconnect, restart the connection cycle + if (Enabled) + Start(); + } + else + { + if (m_activeClientID != Guid.Empty) + m_serverCommandChannel.DisconnectOne(m_activeClientID); + + // When subscriber is in server mode, the server does not need to be restarted, but we + // will reset client statistics - this is a server of "one" client, the publisher + m_activeClientID = Guid.Empty; + m_subscribedDevicesTimer?.Stop(); + m_metadataRefreshPending = false; + m_expectedBufferBlockSequenceNumber = 0u; + m_subscribed = false; + m_keyIVs = null; + TotalBytesReceived = 0L; + m_lastBytesReceived = 0; + m_lastReceivedAt = DateTime.MinValue; + } + } + + // Gets the socket instance used by this client + private Socket? GetCommandChannelSocket() + { + Guid clientID = m_serverCommandChannel?.ClientIDs.FirstOrDefault() ?? Guid.Empty; + + return m_serverCommandChannel switch + { + TcpServer tcpServerCommandChannel when tcpServerCommandChannel.TryGetClient(clientID, out TransportProvider? tcpProvider) => tcpProvider?.Provider, + TlsServer tlsServerCommandChannel when tlsServerCommandChannel.TryGetClient(clientID, out TransportProvider? tlsProvider) => tlsProvider?.Provider?.Socket, +#if NET + _ => (m_clientCommandChannel as TcpClient)?.Client +#else + _ => (m_clientCommandChannel as TcpClient)?.Client ?? (m_clientCommandChannel as TcpSimpleClient)?.Client +#endif + }; + } + + private void HandleDeviceStatisticsRegistration() + { + if (BypassStatistics) + return; + + if (Enabled) + RegisterDeviceStatistics(); + else + UnregisterDeviceStatistics(); + } + + private void RegisterDeviceStatistics() + { + long now = UseLocalClockAsRealTime ? DateTime.UtcNow.Ticks : 0L; + + try + { + DataSet? dataSource = DataSource; + + if (dataSource is null || !dataSource.Tables.Contains("InputStreamDevices")) + { + if (m_statisticsHelpers is not null) + { + foreach (DeviceStatisticsHelper statisticsHelper in m_statisticsHelpers) + statisticsHelper.Device.Dispose(); + } + + m_statisticsHelpers = []; + m_subscribedDevicesLookup = new Dictionary>(); + } + else + { + Dictionary> subscribedDevicesLookup = new(); + List> subscribedDevices = []; + HashSet subscribedDeviceNames = []; + HashSet definedDeviceNames = []; + + foreach (DataRow deviceRow in dataSource.Tables["InputStreamDevices"]!.Select($"ParentID = {ID}")) + definedDeviceNames.Add($"LOCAL${deviceRow["Acronym"].ToNonNullString()}"); + + if (m_statisticsHelpers is not null) + { + foreach (DeviceStatisticsHelper statisticsHelper in m_statisticsHelpers) + { + if (definedDeviceNames.Contains(statisticsHelper.Device.Name)) + { + subscribedDevices.Add(statisticsHelper); + subscribedDeviceNames.Add(statisticsHelper.Device.Name); + } + else + { + statisticsHelper.Device.Dispose(); + } + } + } + + foreach (string definedDeviceName in definedDeviceNames) + { + if (subscribedDeviceNames.Contains(definedDeviceName)) + continue; + + DeviceStatisticsHelper statisticsHelper = new(new SubscribedDevice(definedDeviceName)); + subscribedDevices.Add(statisticsHelper); + statisticsHelper.Reset(now); + } + + if (dataSource.Tables.Contains("ActiveMeasurements")) + { + ActiveMeasurementsTableLookup measurementLookup = DataSourceLookups.ActiveMeasurements(dataSource); + + foreach (DeviceStatisticsHelper statisticsHelper in subscribedDevices) + { + string deviceName = Regex.Replace(statisticsHelper.Device.Name, @"^LOCAL\$", ""); + + foreach (DataRow measurementRow in measurementLookup.LookupByDeviceNameNoStat(deviceName)) + { + if (Guid.TryParse(measurementRow["SignalID"].ToNonNullString(), out Guid signalID)) + { + // In some rare cases duplicate signal ID's have been encountered (likely bad configuration), + // as a result we use a GetOrAdd instead of an Add + subscribedDevicesLookup.GetOrAdd(signalID, statisticsHelper); + + switch (measurementRow["SignalType"].ToNonNullString()) + { + case "FLAG": + statisticsHelper.Device.StatusFlagsID = signalID; + break; + + case "FREQ": + statisticsHelper.Device.FrequencyID = signalID; + break; + + case "DFDT": + statisticsHelper.Device.DeltaFrequencyID = signalID; + break; + } + } + } + } + } + + m_subscribedDevicesLookup = subscribedDevicesLookup; + m_statisticsHelpers = subscribedDevices; + } + + FixExpectedMeasurementCounts(); + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Unable to register device statistics due to exception: {ex.Message}", ex)); + } + } + + private void UnregisterDeviceStatistics() + { + try + { + if (m_statisticsHelpers is null) + return; + + foreach (DeviceStatisticsHelper statisticsHelper in m_statisticsHelpers) + statisticsHelper.Device.Dispose(); + + m_statisticsHelpers = []; + m_subscribedDevicesLookup = new Dictionary>(); + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Unable to unregister device statistics due to exception: {ex.Message}", ex)); + } + } + + private void FixExpectedMeasurementCounts() + { + Dictionary>? subscribedDevicesLookup = m_subscribedDevicesLookup; + List>? statisticsHelpers = m_statisticsHelpers; + DataSet? dataSource = DataSource; + SignalIndexCache? signalIndexCache; + DataTable measurementTable; + + lock (m_signalIndexCacheLock) + signalIndexCache = m_signalIndexCache?[m_cacheIndex]; + + try + { + if (statisticsHelpers is null || subscribedDevicesLookup is null) + return; + + if (signalIndexCache is null) + return; + + if (dataSource is null || !dataSource.Tables.Contains("ActiveMeasurements")) + return; + + measurementTable = dataSource.Tables["ActiveMeasurements"]!; + + if (!measurementTable.Columns.Contains("FramesPerSecond")) + return; + + // Get expected measurement counts + IEnumerable, Guid>> groups = signalIndexCache.AuthorizedSignalIDs + .Where(signalID => subscribedDevicesLookup.TryGetValue(signalID, out _)) + .Select(signalID => Tuple.Create(subscribedDevicesLookup[signalID], signalID)) + .ToList() + .GroupBy(tuple => tuple.Item1, tuple => tuple.Item2); + + foreach (IGrouping, Guid> group in groups) + { + int[] frameRates = group + .Select(signalID => GetFramesPerSecond(measurementTable, signalID)) + .Where(frameRate => frameRate != 0) + .ToArray(); + + group.Key.Device.MeasurementsDefined = frameRates.Length; + group.Key.ExpectedMeasurementsPerSecond = frameRates.Sum(); + } + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Unable to set expected measurement counts for gathering statistics due to exception: {ex.Message}", ex)); + } + } + + private static int GetFramesPerSecond(DataTable measurementTable, Guid signalID) + { + DataRow? row = measurementTable.Select($"SignalID = '{signalID}'").FirstOrDefault(); + + if (row is null) + return 0; + + return row.Field("SignalType")?.ToUpperInvariant() switch + { + "FLAG" => 0, + "STAT" => 0, + "CALC" => 0, + "ALRM" => 0, + "QUAL" => 0, + _ => row.ConvertField("FramesPerSecond") + }; + } + + // This method is called when connection has been authenticated + private void DataSubscriber_ConnectionAuthenticated(object? sender, EventArgs e) + { + if (AutoConnect && Enabled) + StartSubscription(); + } + + // This method is called then new meta-data has been received + private void DataSubscriber_MetaDataReceived(object? sender, EventArgs e) + { + try + { + // We handle synchronization on a separate thread since this process may be lengthy + if (AutoSynchronizeMetadata) + SynchronizeMetadata(e.Argument); + } + catch (Exception ex) + { + // Process exception for logging + OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to queue meta-data synchronization due to exception: {ex.Message}", ex)); + } + } + + /// + /// Raises the event. + /// + protected virtual void OnConnectionEstablished() + { + try + { + ConnectionEstablished?.Invoke(this, EventArgs.Empty); + m_lastMissingCacheWarning = 0L; + } + catch (Exception ex) + { + // We protect our code from consumer thrown exceptions + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ConnectionEstablished event: {ex.Message}", ex), "ConsumerEventException"); + } + } + + /// + /// Raises the event. + /// + protected virtual void OnConnectionTerminated() + { + try + { + ConnectionTerminated?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + // We protect our code from consumer thrown exceptions + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ConnectionTerminated event: {ex.Message}", ex), "ConsumerEventException"); + } + } + + /// + /// Raises the event. + /// + protected virtual void OnConnectionAuthenticated() + { + try + { + ConnectionAuthenticated?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + // We protect our code from consumer thrown exceptions + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ConnectionAuthenticated event: {ex.Message}", ex), "ConsumerEventException"); + } + } + + /// + /// Raises the event. + /// + /// Response received from the server. + /// Command that the server responded to. + protected virtual void OnReceivedServerResponse(ServerResponse responseCode, ServerCommand commandCode) + { + try + { + ReceivedServerResponse?.Invoke(this, new EventArgs(responseCode, commandCode)); + } + catch (Exception ex) + { + // We protect our code from consumer thrown exceptions + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ReceivedServerResponse event: {ex.Message}", ex), "ConsumerEventException"); + } + } + + /// + /// Raises the event. + /// + /// The code for the user command. + /// The code for the server's response. + /// Buffer containing the message from the server. + /// Index into the buffer used to skip the header. + /// The length of the message in the buffer, including the header. + protected virtual void OnReceivedUserCommandResponse(ServerCommand command, ServerResponse response, byte[] buffer, int startIndex, int length) + { + if (ReceivedUserCommandResponse is null) + return; + + try + { + UserCommandArgs args = new(command, response, buffer, startIndex, length); + ReceivedUserCommandResponse?.Invoke(this, args); + } + catch (Exception ex) + { + // We protect our code from consumer thrown exceptions + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for UserCommandResponse event: {ex.Message}", ex), "ConsumerEventException"); + } + } + + /// + /// Raises the event. + /// + /// Meta-data instance to send to client subscription. + protected virtual void OnMetaDataReceived(DataSet metadata) + { + try + { + MetaDataReceived?.Invoke(this, new EventArgs(metadata)); + } + catch (Exception ex) + { + // We protect our code from consumer thrown exceptions + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for MetaDataReceived event: {ex.Message}", ex), "ConsumerEventException"); + } + } + + /// + /// Raises the event. + /// + /// Start time, in , of first measurement transmitted. + protected virtual void OnDataStartTime(Ticks startTime) + { + try + { + DataStartTime?.Invoke(this, new EventArgs(startTime)); + } + catch (Exception ex) + { + // We protect our code from consumer thrown exceptions + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for DataStartTime event: {ex.Message}", ex), "ConsumerEventException"); + } + } + + /// + /// Raises the event. + /// + /// Type name of adapter that sent the processing completed notification. + protected virtual void OnProcessingComplete(string source) + { + try + { + ProcessingComplete?.Invoke(this, new EventArgs(source)); + + // Also raise base class event in case this event has been subscribed + OnProcessingComplete(); + } + catch (Exception ex) + { + // We protect our code from consumer thrown exceptions + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ProcessingComplete event: {ex.Message}", ex), "ConsumerEventException"); + } + } + + /// + /// Raises the event. + /// + /// Message for the notification. + protected virtual void OnNotificationReceived(string message) + { + try + { + NotificationReceived?.Invoke(this, new EventArgs(message)); + } + catch (Exception ex) + { + // We protect our code from consumer thrown exceptions + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for NotificationReceived event: {ex.Message}", ex), "ConsumerEventException"); + } + } + + /// + /// Raises the event. + /// + protected virtual void OnServerConfigurationChanged() + { + try + { + ServerConfigurationChanged?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + // We protect our code from consumer thrown exceptions + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ServerConfigurationChanged event: {ex.Message}", ex), "ConsumerEventException"); + } + } + + /// + /// Raises event. + /// + /// The to assign to this message + /// Processing . + /// A fixed string to classify this event; defaults to null. + /// to use, if any; defaults to . + protected override void OnProcessException(MessageLevel level, Exception exception, string? eventName = null, MessageFlags flags = MessageFlags.None) + { + base.OnProcessException(level, exception, eventName, flags); + + // Just in case Log Message Suppression was turned on, turn it off so this code can raise messages + using IDisposable messages = Logger.OverrideSuppressLogMessages(); + + if (DateTime.UtcNow.Ticks - m_lastParsingExceptionTime > ParsingExceptionWindow) + { + // Exception window has passed since last exception, so we reset counters + m_lastParsingExceptionTime = DateTime.UtcNow.Ticks; + m_parsingExceptionCount = 0; + } + + m_parsingExceptionCount++; + + if (m_parsingExceptionCount <= AllowedParsingExceptions) + return; + + try + { + // When the parsing exception threshold has been exceeded, connection is restarted + Start(); + } + catch (Exception ex) + { + base.OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Error while restarting subscriber connection due to excessive exceptions: {ex.Message}", ex), "DataSubscriber", MessageFlags.UsageIssue); + } + finally + { + // Notify consumer of parsing exception threshold deviation + OnExceededParsingExceptionThreshold(); + m_lastParsingExceptionTime = 0; + m_parsingExceptionCount = 0; + } + } + + /// + /// Raises the event. + /// + private void OnExceededParsingExceptionThreshold() + { + ExceededParsingExceptionThreshold?.Invoke(this, EventArgs.Empty); + } + + // Updates the measurements per second counters after receiving another set of measurements. + private void UpdateMeasurementsPerSecond(DateTime now, int measurementCount) + { + long secondsSinceEpoch = now.Ticks / Ticks.PerSecond; + + if (secondsSinceEpoch > m_lastSecondsSinceEpoch) + { + if (m_measurementsInSecond < MinimumMeasurementsPerSecond || MinimumMeasurementsPerSecond == 0L) + MinimumMeasurementsPerSecond = m_measurementsInSecond; + + if (m_measurementsInSecond > MaximumMeasurementsPerSecond || MaximumMeasurementsPerSecond == 0L) + MaximumMeasurementsPerSecond = m_measurementsInSecond; + + m_totalMeasurementsPerSecond += m_measurementsInSecond; + m_measurementsPerSecondCount++; + m_measurementsInSecond = 0L; + + m_lastSecondsSinceEpoch = secondsSinceEpoch; + } + + m_measurementsInSecond += measurementCount; + } + + // Resets the measurements per second counters after reading the values from the last calculation interval. + private void ResetMeasurementsPerSecondCounters() + { + MinimumMeasurementsPerSecond = 0L; + MaximumMeasurementsPerSecond = 0L; + m_totalMeasurementsPerSecond = 0L; + m_measurementsPerSecondCount = 0L; + } + + private void UpdateStatisticsHelpers() + { + List>? statisticsHelpers = m_statisticsHelpers; + + if (statisticsHelpers is null) + return; + + long now = RealTime; + + foreach (DeviceStatisticsHelper statisticsHelper in statisticsHelpers) + { + statisticsHelper.Update(now); + + // FUTURE: Missing data detection could be complex. For example, no need to continue logging data outages for devices that are offline - but how to detect? + // If data channel is UDP, measurements are missing for time span and data gap recovery enabled, request missing + //if m_dataChannel is not null && m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null && m_lastMeasurementCheck > 0 && + // statisticsHelper.Device.MeasurementsExpected - statisticsHelper.Device.MeasurementsReceived > m_minimumMissingMeasurementThresholdID + // m_dataGapRecoverer.LogDataGap(m_lastMeasurementCheck - Ticks.FromSeconds(m_transmissionDelayTimeAdjustment), now); + } + + //m_lastMeasurementCheck = now; + } + + private void SubscribedDevicesTimer_Elapsed(object? sender, EventArgs elapsedEventArgs) + { + UpdateStatisticsHelpers(); + } + + private bool SynchronizedMetadataChanged(DataSet? newSynchronizedMetadata) + { + try + { + return !DataSetEqualityComparer.Default.Equals(m_synchronizedMetadata, newSynchronizedMetadata); + } + catch + { + return true; + } + } + + /// + /// Gets file path for any defined logging path. + /// + /// Path to acquire within logging path. + /// File path within any defined logging path. + protected string GetLoggingPath(string filePath) + { + return string.IsNullOrWhiteSpace(m_loggingPath) ? FilePath.GetAbsolutePath(filePath) : Path.Combine(m_loggingPath, filePath); + } + + private void DataStreamMonitor_Elapsed(object? sender, EventArgs e) + { + bool dataReceived = m_monitoredBytesReceived > 0; + + if (m_dataChannel is null && m_metadataRefreshPending) + { + if (m_lastReceivedAt > DateTime.MinValue) + dataReceived = (DateTime.UtcNow - m_lastReceivedAt).Seconds < DataLossInterval; + } + + if (!dataReceived) + { + // If we've received no data in the last time-span, we restart connect cycle... + m_dataStreamMonitor!.Enabled = false; + OnStatusMessage(MessageLevel.Info, $"{Environment.NewLine}No data received in {m_dataStreamMonitor.Interval / 1000.0D:0.0} seconds, restarting connect cycle...{Environment.NewLine}", "Connection Issues"); + + ThreadPool.QueueUserWorkItem(_ => + { + if (m_serverCommandChannel is null) + Restart(); + else + DisconnectClient(); + }); + } + + // Reset bytes received bytes being monitored + m_monitoredBytesReceived = 0L; + } + + private void RunTimeLog_ProcessException(object? sender, EventArgs e) + { + OnProcessException(MessageLevel.Info, e.Argument); + } + + private void DataGapRecoverer_RecoveredMeasurements(object? sender, EventArgs> e) + { + OnNewMeasurements(e.Argument); + } + + private void DataGapRecoverer_StatusMessage(object? sender, EventArgs e) + { + OnStatusMessage(MessageLevel.Info, "[DataGapRecoverer] " + e.Argument); + } + + private void DataGapRecoverer_ProcessException(object? sender, EventArgs e) + { + OnProcessException(MessageLevel.Warning, new InvalidOperationException("[DataGapRecoverer] " + e.Argument.Message, e.Argument.InnerException)); + } + + #region [ Client Command Channel Event Handlers ] + + private void ClientCommandChannelConnectionEstablished(object? sender, EventArgs e) + { + // Define operational modes as soon as possible + SendServerCommand(ServerCommand.DefineOperationalModes, BigEndian.GetBytes((uint)m_operationalModes)); + + // Notify input adapter base that asynchronous connection succeeded + if (!PersistConnectionForMetadata) + OnConnected(); + else + SendServerCommand(ServerCommand.MetaDataRefresh, MetadataFilters); + + // Notify consumer that connection was successfully established + OnConnectionEstablished(); + + OnStatusMessage(MessageLevel.Info, m_serverCommandChannel is null ? "Data subscriber command channel connection to publisher was established." : "Data subscriber server-based command channel established a new client connection from the publisher."); + + if (AutoConnect && Enabled) + StartSubscription(); + + if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null) + m_dataGapRecoverer.Enabled = true; + } + + private void ClientCommandChannelConnectionTerminated(object? sender, EventArgs e) + { + OnConnectionTerminated(); + + OnStatusMessage(MessageLevel.Info, m_serverCommandChannel is null ? "Data subscriber command channel connection to publisher was terminated." : "Data subscriber server-based command channel client connection from the publisher was terminated."); + + DisconnectClient(); + } + + private void ClientCommandChannelConnectionException(object? sender, EventArgs e) + { + Exception ex = e.Argument; + OnProcessException(MessageLevel.Info, new ConnectionException($"Data subscriber encountered an exception while attempting command channel publisher connection: {ex.Message}", ex)); + } + + private void ClientCommandChannelConnectionAttempt(object? sender, EventArgs e) + { + // Inject a short delay between multiple connection attempts + if (m_commandChannelConnectionAttempts > 0) + Thread.Sleep(2000); + + OnStatusMessage(MessageLevel.Info, "Attempting command channel connection to publisher..."); + m_commandChannelConnectionAttempts++; + } + + private void ClientCommandChannelSendDataException(object? sender, EventArgs e) + { + Exception ex = e.Argument; + + if (!HandleSocketException(ex) && ex is not ObjectDisposedException) + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data subscriber encountered an exception while sending command channel data to publisher connection: {ex.Message}", ex)); + } + + private void ClientCommandChannelReceiveData(object? sender, EventArgs e) + { + try + { + int length = e.Argument; + byte[] buffer = new byte[length]; + + m_lastBytesReceived = length; + m_lastReceivedAt = DateTime.UtcNow; + + m_clientCommandChannel?.Read(buffer, 0, length); + m_serverCommandChannel?.Read(m_activeClientID, buffer, 0, length); + + ProcessServerResponse(buffer, length); + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Info, ex); + } + } + + private void ClientCommandChannelReceiveDataException(object? sender, EventArgs e) + { + Exception ex = e.Argument; + + if (!HandleSocketException(ex) && ex is not ObjectDisposedException) + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data subscriber encountered an exception while receiving command channel data from publisher connection: {ex.Message}", ex)); + } + + #endregion + + #region [ Server Command Channel Event Handlers ] + + private void ServerCommandChannelReceiveClientData(object? sender, EventArgs e) + { + ClientCommandChannelReceiveData(sender, new EventArgs(e.Argument2)); + } + + private void ServerCommandChannelClientConnected(object? sender, EventArgs e) + { + m_activeClientID = e.Argument; + + // Reset all connection stats when new publisher-client connects - this is equivalent + // to a normal client-based subscriber establishing a new connection to the publisher + List>? statisticsHelpers = m_statisticsHelpers; + + if (statisticsHelpers is not null) + { + long now = UseLocalClockAsRealTime ? DateTime.UtcNow.Ticks : 0L; + m_realTime = 0L; + m_lastStatisticsHelperUpdate = 0L; + + foreach (DeviceStatisticsHelper statisticsHelper in statisticsHelpers) + statisticsHelper.Reset(now); + } + + if (UseLocalClockAsRealTime) + m_subscribedDevicesTimer?.Start(); + + ClientCommandChannelConnectionEstablished(sender, EventArgs.Empty); + } + + private void ServerCommandChannelClientDisconnected(object? sender, EventArgs e) + { + ClientCommandChannelConnectionTerminated(sender, EventArgs.Empty); + } + + private void ServerCommandChannelClientConnectingException(object? sender, EventArgs e) + { + Exception ex = e.Argument; + OnProcessException(MessageLevel.Info, new ConnectionException($"Data subscriber encountered an exception while connecting client-based publisher to the command channel: {ex.Message}", ex)); + } + + private void ServerCommandChannelServerStarted(object? sender, EventArgs e) + { + OnStatusMessage(MessageLevel.Info, "Data subscriber server-based command channel started."); + } + + private void ServerCommandChannelServerStopped(object? sender, EventArgs e) + { + if (Enabled) + { + OnStatusMessage(MessageLevel.Info, "Data subscriber server-based command channel was unexpectedly terminated, restarting..."); + + Action restartServerCommandChannel = () => + { + try + { + m_serverCommandChannel?.Start(); + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to restart data subscriber server-based command channel: {ex.Message}", ex)); + } + }; + + // We must wait for command channel to completely shutdown before trying to restart... + restartServerCommandChannel.DelayAndExecute(2000); + } + else + { + OnStatusMessage(MessageLevel.Info, "Data subscriber server-based command channel stopped."); + } + } + + private void ServerCommandChannelSendClientDataException(object? sender, EventArgs e) + { + Exception ex = e.Argument2; + + if (HandleSocketException(ex)) + return; + + if (ex is not NullReferenceException && ex is not ObjectDisposedException) + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data subscriber encountered an exception while sending command channel data to client-based publisher connection: {ex.Message}", ex)); + + DisconnectClient(); + } + + private void ServerCommandChannelReceiveClientDataException(object? sender, EventArgs e) + { + Exception ex = e.Argument2; + + if (HandleSocketException(ex)) + return; + + if (ex is not NullReferenceException && ex is not ObjectDisposedException) + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data subscriber encountered an exception while receiving command channel data from client-based publisher connection: {ex.Message}", ex)); + + DisconnectClient(); + } + + #endregion + + #region [ Data Channel Event Handlers ] + + private void DataChannelConnectionException(object? sender, EventArgs e) + { + Exception ex = e.Argument; + OnProcessException(MessageLevel.Info, new ConnectionException($"Data subscriber encountered an exception while attempting to establish UDP data channel connection: {ex.Message}", ex)); + } + + private void DataChannelConnectionAttempt(object? sender, EventArgs e) + { + // Inject a short delay between multiple connection attempts + if (m_dataChannelConnectionAttempts > 0) + Thread.Sleep(2000); + + OnStatusMessage(MessageLevel.Info, "Attempting to establish data channel connection to publisher..."); + m_dataChannelConnectionAttempts++; + } + + private void DataChannelReceiveData(object? sender, EventArgs e) + { + try + { + int length = e.Argument; + byte[] buffer = new byte[length]; + + m_lastBytesReceived = length; + + m_dataChannel?.Read(buffer, 0, length); + ProcessServerResponse(buffer, length); + } + catch (Exception ex) + { + OnProcessException(MessageLevel.Info, ex); + } + } + + private void DataChannelReceiveDataException(object? sender, EventArgs e) + { + Exception ex = e.Argument; + + if (!HandleSocketException(ex) && ex is not ObjectDisposedException) + OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data subscriber encountered an exception while receiving UDP data from publisher connection: {ex.Message}", ex)); + } + + #endregion + +#endregion + + #region [ Static ] + + // Static Properties + private static readonly ServerCommand[] s_userCommands = + [ + ServerCommand.UserCommand00, + ServerCommand.UserCommand01, + ServerCommand.UserCommand02, + ServerCommand.UserCommand03, + ServerCommand.UserCommand04, + ServerCommand.UserCommand05, + ServerCommand.UserCommand06, + ServerCommand.UserCommand07, + ServerCommand.UserCommand08, + ServerCommand.UserCommand09, + ServerCommand.UserCommand10, + ServerCommand.UserCommand11, + ServerCommand.UserCommand12, + ServerCommand.UserCommand13, + ServerCommand.UserCommand14, + ServerCommand.UserCommand15 + ]; + + // Static Methods + + /// + /// Gets the path to the local certificate from the configuration file. + /// + /// Path to the local certificate from the configuration file. + internal static string? GetLocalCertificate() + { + try + { + // ReSharper disable once JoinDeclarationAndInitializer + // ReSharper disable once RedundantAssignment + string localCertificate = null!; + +#if NET + localCertificate = ConfigSettings.Default[ConfigSettings.SystemSettingsCategory]["LocalCertificate"]; +#else + CategorizedSettingsElement localCertificateElement = ConfigurationFile.Current.Settings["systemSettings"]["LocalCertificate"]; + + if (localCertificateElement is not null) + localCertificate = localCertificateElement.Value; +#endif + + if (localCertificate is null || !File.Exists(FilePath.GetAbsolutePath(localCertificate))) + throw new InvalidOperationException("Unable to find local certificate. Local certificate file must exist when using TLS security mode."); + + return localCertificate; + } + catch (Exception ex) + { + Logger.SwallowException(ex, nameof(GetLocalCertificate)); + return null; + } + } + + /// + /// Checks if the specified certificate exists, updating path if needed. + /// + /// Reference certificate. + /// true if certificate exists; otherwise, false. + internal static bool RemoteCertificateExists(ref string remoteCertificate) + { + try + { + if (File.Exists(FilePath.GetAbsolutePath(remoteCertificate))) + return true; + +#if NET + string remoteCertificatePath = ConfigSettings.Default[ConfigSettings.SystemSettingsCategory]["RemoteCertificatesPath"]; + + if (string.IsNullOrWhiteSpace(remoteCertificatePath)) + return false; +#else + CategorizedSettingsElement remoteCertificateElement = ConfigurationFile.Current.Settings["systemSettings"]["RemoteCertificatesPath"]; + + if (remoteCertificateElement is null) + return false; + + string remoteCertificatePath = remoteCertificateElement.Value; +#endif + + remoteCertificate = Path.Combine(remoteCertificatePath, remoteCertificate); + + return File.Exists(FilePath.GetAbsolutePath(remoteCertificate)); + } + catch (Exception ex) + { + Logger.SwallowException(ex, nameof(RemoteCertificateExists)); + return false; + } + } + + #endregion } \ No newline at end of file diff --git a/src/lib/SignalIndexCache.cs b/src/lib/sttp.core/SignalIndexCache.cs similarity index 95% rename from src/lib/SignalIndexCache.cs rename to src/lib/sttp.core/SignalIndexCache.cs index abf2f83a..52accd2f 100644 --- a/src/lib/SignalIndexCache.cs +++ b/src/lib/sttp.core/SignalIndexCache.cs @@ -23,16 +23,6 @@ // //****************************************************************************************************** -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Text; -using GSF; -using GSF.Collections; -using GSF.Parsing; -using GSF.TimeSeries; using sttp.tssc; namespace sttp; @@ -67,20 +57,20 @@ public class SignalIndexCache : ISupportBinaryImage // Since measurement keys are statically cached as a global system optimization and the keys // can be different between two parties exchanging data, the raw measurement key elements are // cached and exchanged instead of actual measurement key values - private ConcurrentDictionary m_reference; - private Guid[] m_unauthorizedSignalIDs; + private ConcurrentDictionary m_reference = default!; + private Guid[] m_unauthorizedSignalIDs = []; /// /// Lookups MeasurementKey.RuntimeID and returns int SignalIndex. -1 means it does not exist. /// [NonSerialized] // SignalID reverse lookup runtime cache (used to speed deserialization) - private IndexedArray m_signalIDCache; + private IndexedArray m_signalIDCache = default!; [NonSerialized] - private Encoding m_encoding; + private Encoding? m_encoding; [NonSerialized] - internal TsscDecoder TsscDecoder; + internal TsscDecoder? TsscDecoder; #endregion @@ -100,7 +90,7 @@ public SignalIndexCache() /// /// based data source used to interpret local measurement keys. /// Deserialized remote signal index cache. - public SignalIndexCache(DataSet dataSource, SignalIndexCache remoteCache) + public SignalIndexCache(DataSet? dataSource, SignalIndexCache remoteCache) { m_subscriberID = remoteCache.SubscriberID; @@ -113,7 +103,7 @@ public SignalIndexCache(DataSet dataSource, SignalIndexCache remoteCache) } else { - DataTable activeMeasurements = dataSource.Tables["ActiveMeasurements"]; + DataTable activeMeasurements = dataSource.Tables["ActiveMeasurements"]!; ConcurrentDictionary reference = new(); foreach (KeyValuePair signalIndex in remoteCache.Reference) @@ -193,7 +183,7 @@ public Guid[] UnauthorizedSignalIDs /// /// Gets or sets character encoding used to convert strings to binary. /// - public Encoding Encoding + public Encoding? Encoding { get => m_encoding; set => m_encoding = value; @@ -225,7 +215,7 @@ public int BinaryLength binaryLength += 4; // Each unauthorized ID - binaryLength += 16 * (m_unauthorizedSignalIDs ?? []).Length; + binaryLength += 16 * m_unauthorizedSignalIDs.Length; return binaryLength; } @@ -257,7 +247,7 @@ public int GenerateBinaryImage(byte[] buffer, int startIndex) if (m_encoding is null) throw new InvalidOperationException("Attempt to generate binary image of signal index cache without setting a character encoding."); - Guid[] unauthorizedSignalIDs = m_unauthorizedSignalIDs ?? []; + Guid[] unauthorizedSignalIDs = m_unauthorizedSignalIDs; int binaryLength = BinaryLength; int offset = startIndex; @@ -346,7 +336,7 @@ public int ParseBinaryImage(byte[] buffer, int startIndex, int length) if (length < binaryLength) return 0; - // We know we have enough data so we can empty the reference cache + // We know we have enough data, so we can empty the reference cache m_reference.Clear(); // Subscriber ID diff --git a/src/lib/SubscriberAdapter.cs b/src/lib/sttp.core/SubscriberAdapter.cs similarity index 89% rename from src/lib/SubscriberAdapter.cs rename to src/lib/sttp.core/SubscriberAdapter.cs index 24a7ee74..a1a5e3df 100644 --- a/src/lib/SubscriberAdapter.cs +++ b/src/lib/sttp.core/SubscriberAdapter.cs @@ -22,24 +22,11 @@ // Modified Header. // //****************************************************************************************************** +// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident +// ReSharper disable PossibleMultipleEnumeration -using GSF; -using GSF.Diagnostics; -using GSF.IO; -using GSF.Parsing; -using GSF.Threading; -using GSF.TimeSeries; -using GSF.TimeSeries.Adapters; -using GSF.TimeSeries.Transport; using sttp.tssc; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -// ReSharper disable PossibleMultipleEnumeration namespace sttp; /// @@ -55,7 +42,7 @@ internal class SubscriberAdapter : FacileActionAdapterBase, IClientSubscription /// Indicates that a buffer block needed to be retransmitted because /// it was previously sent, but no confirmation was received. /// - public event EventHandler BufferBlockRetransmission; + public event EventHandler? BufferBlockRetransmission; /// /// Indicates to the host that processing for an input adapter (via temporal session) has completed. @@ -64,40 +51,48 @@ internal class SubscriberAdapter : FacileActionAdapterBase, IClientSubscription /// This event is expected to only be raised when an input adapter has been designed to process /// a finite amount of data, e.g., reading a historical range of data during temporal processing. /// - public event EventHandler> ProcessingComplete; + public event EventHandler>? ProcessingComplete; // Fields - private DataPublisher m_parent; + private readonly DataPublisher m_parent; private readonly SubscriberConnection m_connection; private volatile bool m_usePayloadCompression; private volatile bool m_useCompactMeasurementFormat; private readonly CompressionModes m_compressionModes; private bool m_resetTsscEncoder; - private TsscEncoder m_tsscEncoder; + private TsscEncoder? m_tsscEncoder; +#if NET + private readonly Lock m_tsscSyncLock; +#else private readonly object m_tsscSyncLock; - private byte[] m_tsscWorkingBuffer; +#endif + private byte[] m_tsscWorkingBuffer = default!; private ushort m_tsscSequenceNumber; private long m_lastPublishTime; private double m_publishInterval; private bool m_includeTime; private bool m_useMillisecondResolution; private bool m_isNaNFiltered; - private volatile long[] m_baseTimeOffsets; + private volatile long[]? m_baseTimeOffsets; private volatile int m_timeIndex; - private SharedTimer m_baseTimeRotationTimer; + private SharedTimer? m_baseTimeRotationTimer; private volatile bool m_startTimeSent; - private IaonSession m_iaonSession; + private IaonSession? m_iaonSession; - private readonly List m_bufferBlockCache; + private readonly List m_bufferBlockCache; +#if NET + private readonly Lock m_bufferBlockCacheLock; +#else private readonly object m_bufferBlockCacheLock; +#endif private uint m_bufferBlockSequenceNumber; private uint m_expectedBufferBlockConfirmationNumber; - private SharedTimer m_bufferBlockRetransmissionTimer; + private SharedTimer m_bufferBlockRetransmissionTimer = default!; private double m_bufferBlockRetransmissionTimeout; private bool m_disposed; - #endregion +#endregion #region [ Constructors ] @@ -115,13 +110,10 @@ public SubscriberAdapter(DataPublisher parent, Guid clientID, Guid subscriberID, SubscriberID = subscriberID; m_compressionModes = compressionModes; m_bufferBlockCache = []; - m_bufferBlockCacheLock = new object(); - m_tsscSyncLock = new object(); - m_parent.ClientConnections.TryGetValue(ClientID, out m_connection); - - if (m_connection is null) - throw new NullReferenceException("Subscriber adapter failed to find associated connection"); - + m_bufferBlockCacheLock = new(); + m_tsscSyncLock = new(); + m_parent.ClientConnections.TryGetValue(ClientID, out SubscriberConnection? connection); + m_connection = connection ?? throw new NullReferenceException("Subscriber adapter failed to find associated connection"); m_connection.SignalIndexCache = new SignalIndexCache { SubscriberID = subscriberID }; } @@ -155,7 +147,7 @@ public override string Name /// /// Gets the input filter requested by the subscriber when establishing this . /// - public string RequestedInputFilter { get; private set; } + public string? RequestedInputFilter { get; private set; } /// /// Gets or sets flag that determines if payload compression should be enabled in data packets of this . @@ -229,7 +221,7 @@ public override int ProcessingInterval /// We override method so assignment can be synchronized such that dynamic updates won't interfere /// with filtering in . /// - public override MeasurementKey[] InputMeasurementKeys + public override MeasurementKey[]? InputMeasurementKeys { get => base.InputMeasurementKeys; set @@ -238,7 +230,7 @@ public override MeasurementKey[] InputMeasurementKeys { // Update signal index cache unless "detaching" from real-time if (value is not null && !(value.Length == 1 && value[0] == MeasurementKey.Undefined) && - value.Length > 0 && !new HashSet(base.InputMeasurementKeys).SetEquals(value)) + value.Length > 0 && !new HashSet(base.InputMeasurementKeys ?? []).SetEquals(value)) { // Safe: no lock required for signal index cache here Guid[] authorizedSignalIDs = m_parent.UpdateSignalIndexCache(ClientID, m_connection.SignalIndexCache, value); @@ -286,11 +278,15 @@ public override string Status /// /// Gets the status of the active temporal session, if any. /// - public string TemporalSessionStatus => m_iaonSession?.Status; + public string? TemporalSessionStatus => m_iaonSession?.Status; int IClientSubscription.CompressionStrength { get; set; } = 31; +#if NET + object? IClientSubscription.SignalIndexCache => null; +#else GSF.TimeSeries.Transport.SignalIndexCache IClientSubscription.SignalIndexCache => null; +#endif #endregion @@ -310,8 +306,6 @@ protected override void Dispose(bool disposing) if (!disposing) return; - m_parent = null; - // Dispose base time rotation timer if (m_baseTimeRotationTimer is not null) { @@ -337,7 +331,7 @@ public override void Initialize() Dictionary settings = Settings; MeasurementKey[] inputMeasurementKeys; - if (settings.TryGetValue(nameof(SubscriptionInfo.FilterExpression), out string setting)) + if (settings.TryGetValue(nameof(SubscriptionInfo.FilterExpression), out string? setting)) { // IMPORTANT: The allowSelect argument of ParseInputMeasurementKeys must be null // in order to prevent SQL injection via the subscription filter expression @@ -454,8 +448,8 @@ public override string GetShortStatus(int maxLength) /// so that dynamic updates to keys will be synchronized with filtering to prevent interference. /// // IMPORTANT: TSSC is sensitive to order - always make sure this function gets called sequentially, concurrent - // calls to this function can cause TSSC parsing to get out of sequence and fail - public override void QueueMeasurementsForProcessing(IEnumerable measurements) + // calls to this function can cause TSSC parsing to get out of sequence and /fail + public override void QueueMeasurementsForProcessing(IEnumerable? measurements) { if (measurements is null) return; @@ -464,7 +458,7 @@ public override void QueueMeasurementsForProcessing(IEnumerable me { m_startTimeSent = true; - IMeasurement measurement = measurements.FirstOrDefault(m => m is not null); + IMeasurement? measurement = measurements.FirstOrDefault(); Ticks timestamp = 0; if (measurement is not null) @@ -525,8 +519,6 @@ public override void QueueMeasurementsForProcessing(IEnumerable me /// A list of buffer block sequence numbers for blocks that need to be retransmitted. public void ConfirmBufferBlock(uint sequenceNumber) { - DataPublisher parent = m_parent; - // We are still receiving confirmations, // so stop the retransmission timer m_bufferBlockRetransmissionTimer.Stop(); @@ -560,7 +552,7 @@ public void ConfirmBufferBlock(uint sequenceNumber) if (m_bufferBlockCache[i] is null) continue; - parent?.SendClientResponse(ClientID, ServerResponse.BufferBlock, ServerCommand.Subscribe, m_bufferBlockCache[i]); + m_parent?.SendClientResponse(ClientID, ServerResponse.BufferBlock, ServerCommand.Subscribe, m_bufferBlockCache[i]); OnBufferBlockRetransmission(); } } @@ -580,10 +572,10 @@ public void ConfirmSignalIndexCache(Guid clientID) // Swap over to next signal index cache if (m_connection.NextSignalIndexCache is not null) { - OnStatusMessage(MessageLevel.Info, $"Received confirmation of signal index cache update for subscriber {clientID}. Transitioning from cache index {m_connection.CurrentCacheIndex} with {m_connection.SignalIndexCache?.Reference?.Count ?? 0:N0} records to cache index {m_connection.NextCacheIndex} with {m_connection.NextSignalIndexCache.Reference?.Count ?? 0:N0} records...", nameof(ConfirmSignalIndexCache)); + OnStatusMessage(MessageLevel.Info, $"Received confirmation of signal index cache update for subscriber {clientID}. Transitioning from cache index {m_connection.CurrentCacheIndex} with {m_connection.SignalIndexCache?.Reference.Count ?? 0:N0} records to cache index {m_connection.NextCacheIndex} with {m_connection.NextSignalIndexCache?.Reference.Count ?? 0:N0} records...", nameof(ConfirmSignalIndexCache)); m_connection.SignalIndexCache = m_connection.NextSignalIndexCache; - m_connection.SignalIndexCache.SubscriberID = SubscriberID; + m_connection.SignalIndexCache!.SubscriberID = SubscriberID; m_connection.CurrentCacheIndex = m_connection.NextCacheIndex; m_connection.NextSignalIndexCache = null; @@ -644,7 +636,7 @@ private void ProcessMeasurements(IEnumerable measurements) //usePayloadCompression = m_usePayloadCompression; bool useCompactMeasurementFormat = m_useCompactMeasurementFormat; - SignalIndexCache signalIndexCache; + SignalIndexCache? signalIndexCache; int currentCacheIndex; lock (m_connection.CacheUpdateLock) @@ -658,7 +650,7 @@ private void ProcessMeasurements(IEnumerable measurements) foreach (IMeasurement measurement in measurements) { - if (measurement is BufferBlockMeasurement bufferBlockMeasurement) + if (measurement is BufferBlockMeasurement { Buffer: not null } bufferBlockMeasurement) { // Still sending buffer block measurements to client; we are expecting // confirmations which will indicate whether retransmission is necessary, @@ -699,7 +691,7 @@ private void ProcessMeasurements(IEnumerable measurements) // Serialize the current measurement. IBinaryMeasurement binaryMeasurement = useCompactMeasurementFormat ? new CompactMeasurement(measurement, signalIndexCache, m_includeTime, m_baseTimeOffsets, m_timeIndex, m_useMillisecondResolution) : - new SerializableMeasurement(measurement, m_parent.GetClientEncoding(ClientID)); + throw new InvalidOperationException("Full measurement serialization not supported."); // Determine the size of the measurement in bytes. int binaryLength = binaryMeasurement.BinaryLength; @@ -762,7 +754,7 @@ private void ProcessBinaryMeasurements(IEnumerable measureme measurement.CopyBinaryImageToStream(workingBuffer); // Publish data packet to client - m_parent?.SendClientResponse(ClientID, ServerResponse.DataPacket, ServerCommand.Subscribe, workingBuffer.ToArray()); + m_parent.SendClientResponse(ClientID, ServerResponse.DataPacket, ServerCommand.Subscribe, workingBuffer.ToArray()); // Track last publication time m_lastPublishTime = DateTime.UtcNow.Ticks; @@ -770,7 +762,7 @@ private void ProcessBinaryMeasurements(IEnumerable measureme private void ProcessTSSCMeasurements(IEnumerable measurements) { - SignalIndexCache signalIndexCache; + SignalIndexCache? signalIndexCache; int currentCacheIndex; lock (m_connection.CacheUpdateLock) @@ -808,20 +800,20 @@ private void ProcessTSSCMeasurements(IEnumerable measurements) { int index = signalIndexCache.GetSignalIndex(measurement.Key); - if (index < int.MaxValue) // Ignore unmapped signal + if (index == int.MaxValue) + continue; // Ignore unmapped signal + + if (!m_tsscEncoder.TryAddMeasurement(index, measurement.Timestamp.Value, (uint)measurement.StateFlags, (float)measurement.AdjustedValue)) { - if (!m_tsscEncoder.TryAddMeasurement(index, measurement.Timestamp.Value, (uint)measurement.StateFlags, (float)measurement.AdjustedValue)) - { - SendTSSCPayload(count, currentCacheIndex); - count = 0; - m_tsscEncoder.SetBuffer(m_tsscWorkingBuffer, 0, m_tsscWorkingBuffer.Length); - - // This will always succeed - m_tsscEncoder.TryAddMeasurement(index, measurement.Timestamp.Value, (uint)measurement.StateFlags, (float)measurement.AdjustedValue); - } + SendTSSCPayload(count, currentCacheIndex); + count = 0; + m_tsscEncoder.SetBuffer(m_tsscWorkingBuffer, 0, m_tsscWorkingBuffer.Length); - count++; + // This will always succeed + m_tsscEncoder.TryAddMeasurement(index, measurement.Timestamp.Value, (uint)measurement.StateFlags, (float)measurement.AdjustedValue); } + + count++; } if (count > 0) @@ -830,7 +822,7 @@ private void ProcessTSSCMeasurements(IEnumerable measurements) IncrementProcessedMeasurements(measurements.Count()); // Update latency statistics - m_parent?.UpdateLatencyStatistics(measurements.Select(m => (long)(m_lastPublishTime - m.Timestamp))); + m_parent.UpdateLatencyStatistics(measurements.Select(m => (long)(m_lastPublishTime - m.Timestamp))); } catch (Exception ex) { @@ -842,6 +834,9 @@ private void ProcessTSSCMeasurements(IEnumerable measurements) private void SendTSSCPayload(int count, int currentCacheIndex) { + if (m_tsscEncoder is null) + return; + int length = m_tsscEncoder.FinishBlock(); byte[] packet = new byte[length + 8]; DataPacketFlags flags = DataPacketFlags.Compressed; @@ -865,18 +860,18 @@ private void SendTSSCPayload(int count, int currentCacheIndex) Array.Copy(m_tsscWorkingBuffer, 0, packet, 8, length); - m_parent?.SendClientResponse(ClientID, ServerResponse.DataPacket, ServerCommand.Subscribe, packet); + m_parent.SendClientResponse(ClientID, ServerResponse.DataPacket, ServerCommand.Subscribe, packet); // Track last publication time m_lastPublishTime = DateTime.UtcNow.Ticks; } // Retransmits all buffer blocks for which confirmation has not yet been received - private void BufferBlockRetransmissionTimer_Elapsed(object sender, EventArgs e) + private void BufferBlockRetransmissionTimer_Elapsed(object? sender, EventArgs e) { lock (m_bufferBlockCacheLock) { - foreach (byte[] bufferBlock in m_bufferBlockCache) + foreach (byte[]? bufferBlock in m_bufferBlockCache) { if (bufferBlock is null) continue; @@ -893,7 +888,7 @@ private void BufferBlockRetransmissionTimer_Elapsed(object sender, EventArgs e) + private void BaseTimeRotationTimer_Elapsed(object? sender, EventArgs e) { RotateBaseTimes(); } - void IClientSubscription.OnStatusMessage(MessageLevel level, string status, string eventName, MessageFlags flags) + void IClientSubscription.OnStatusMessage(MessageLevel level, string status, string? eventName, MessageFlags flags) { OnStatusMessage(level, status, eventName, flags); } - void IClientSubscription.OnProcessException(MessageLevel level, Exception ex, string eventName, MessageFlags flags) + void IClientSubscription.OnProcessException(MessageLevel level, Exception ex, string? eventName, MessageFlags flags) { OnProcessException(level, ex, eventName, flags); } // Explicitly implement processing completed event bubbler to satisfy IClientSubscription interface - void IClientSubscription.OnProcessingCompleted(object sender, EventArgs e) + void IClientSubscription.OnProcessingCompleted(object? sender, EventArgs e) { ProcessingComplete?.Invoke(sender, new EventArgs(this, e)); } diff --git a/src/lib/SubscriberConnection.cs b/src/lib/sttp.core/SubscriberConnection.cs similarity index 86% rename from src/lib/SubscriberConnection.cs rename to src/lib/sttp.core/SubscriberConnection.cs index cdb6f22f..c8435b53 100644 --- a/src/lib/SubscriberConnection.cs +++ b/src/lib/sttp.core/SubscriberConnection.cs @@ -22,19 +22,7 @@ // Modified Header. // //****************************************************************************************************** - -using GSF; -using GSF.Communication; -using GSF.Diagnostics; -using GSF.IO; -using GSF.Threading; -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Security.Cryptography; -using System.Text; -using TcpClient = GSF.Communication.TcpClient; +// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident namespace sttp; @@ -52,23 +40,23 @@ public class SubscriberConnection : IProvideStatus, IDisposable private const int IVIndex = 1; // Index of initialization vector component in keyIV array // Fields - private DataPublisher m_parent; + private readonly DataPublisher m_parent; private Guid m_subscriberID; private string m_connectionID; private readonly string m_hostName; private readonly IPAddress m_ipAddress; - private string m_subscriberInfo; - private SubscriberAdapter m_subscription; + private string? m_subscriberInfo; + private SubscriberAdapter? m_subscription; private volatile bool m_authenticated; - private volatile byte[][][] m_keyIVs; + private volatile byte[][][]? m_keyIVs; private volatile int m_cipherIndex; - private UdpServer m_dataChannel; - private string m_configurationString; + private UdpServer? m_dataChannel; + private string? m_configurationString; private bool m_connectionEstablished; - private SharedTimer m_pingTimer; - private SharedTimer m_reconnectTimer; + private readonly SharedTimer m_pingTimer; + private readonly SharedTimer m_reconnectTimer; private OperationalModes m_operationalModes; - private Encoding m_encoding; + private Encoding? m_encoding; private bool m_disposed; #endregion @@ -81,8 +69,8 @@ public class SubscriberConnection : IProvideStatus, IDisposable /// Parent data publisher. /// Client ID of associated connection. /// command channel used to lookup connection information. - /// command channel used to lookup connection information. - public SubscriberConnection(DataPublisher parent, Guid clientID, IServer serverCommandChannel, IClient clientCommandChannel) + /// command channel used to lookup connection information. + public SubscriberConnection(DataPublisher parent, Guid clientID, IServer? serverCommandChannel, IClient? clientCommandChannel) { m_parent = parent; ClientID = clientID; @@ -91,8 +79,8 @@ public SubscriberConnection(DataPublisher parent, Guid clientID, IServer serverC m_subscriberID = clientID; m_keyIVs = null; m_cipherIndex = 0; - CacheUpdateLock = new object(); - PendingCacheUpdateLock = new object(); + CacheUpdateLock = new(); + PendingCacheUpdateLock = new(); PublishBuffer = new BlockAllocatedMemoryStream(); // Setup ping timer @@ -106,7 +94,7 @@ public SubscriberConnection(DataPublisher parent, Guid clientID, IServer serverC m_reconnectTimer.AutoReset = false; m_reconnectTimer.Elapsed += ReconnectTimer_Elapsed; - LookupEndPointInfo(clientID, GetCommandChannelSocket().RemoteEndPoint as IPEndPoint, ref m_ipAddress, ref m_hostName, ref m_connectionID); + LookupEndPointInfo(clientID, GetCommandChannelSocket()?.RemoteEndPoint as IPEndPoint, ref m_ipAddress!, ref m_hostName!, ref m_connectionID!); } /// @@ -134,17 +122,21 @@ public SubscriberConnection(DataPublisher parent, Guid clientID, IServer serverC /// /// Gets the current signal index cache of this . /// - public SignalIndexCache SignalIndexCache { get; internal set; } + public SignalIndexCache? SignalIndexCache { get; internal set; } /// /// Gets the pending Signal Index Cache. /// - public SignalIndexCache NextSignalIndexCache { get; internal set; } + public SignalIndexCache? NextSignalIndexCache { get; internal set; } /// /// Gets the lock object for updating Signal Index Cache properties. /// +#if NET + internal Lock CacheUpdateLock { get; } +#else internal object CacheUpdateLock { get; } +#endif /// /// Gets the current Signal Index Cache index, i.e., zero or one. @@ -159,7 +151,7 @@ public SubscriberConnection(DataPublisher parent, Guid clientID, IServer serverC /// /// Gets or sets reference to data channel, attaching to or detaching from events as needed, associated with this . /// - public UdpServer DataChannel + public UdpServer? DataChannel { get => m_dataChannel; set @@ -198,17 +190,17 @@ public UdpServer DataChannel /// /// Gets command channel. /// - public IServer ServerCommandChannel { get; private set; } + public IServer? ServerCommandChannel { get; private set; } /// /// Gets command channel. /// - public IClient ClientCommandChannel { get; private set; } + public IClient? ClientCommandChannel { get; private set; } /// /// Gets publication channel - that is, data channel if defined otherwise command channel. /// - public IServer ServerPublishChannel => m_dataChannel ?? ServerCommandChannel; + public IServer? ServerPublishChannel => m_dataChannel ?? ServerCommandChannel; /// /// Gets buffer used to hold publish data for the . @@ -231,7 +223,7 @@ public bool IsConnected try { - Socket commandChannelSocket = GetCommandChannelSocket(); + Socket? commandChannelSocket = GetCommandChannelSocket(); if (commandChannelSocket is not null) isConnected = commandChannelSocket.Connected; @@ -281,19 +273,19 @@ public Guid SubscriberID /// /// Gets or sets the subscriber acronym of this . /// - public string SubscriberAcronym { get; set; } + public string? SubscriberAcronym { get; set; } /// /// Gets or sets the subscriber name of this . /// - public string SubscriberName { get; set; } + public string? SubscriberName { get; set; } /// /// Gets or sets subscriber info for this . /// public string SubscriberInfo { - get => string.IsNullOrWhiteSpace(m_subscriberInfo) ? SubscriberName : m_subscriberInfo; + get => string.IsNullOrWhiteSpace(m_subscriberInfo) ? SubscriberName ?? "undefined" : m_subscriberInfo; set { if (string.IsNullOrWhiteSpace(value)) @@ -304,9 +296,9 @@ public string SubscriberInfo { Dictionary settings = value.ParseKeyValuePairs(); - settings.TryGetValue("source", out string source); - settings.TryGetValue("version", out string version); - settings.TryGetValue("updatedOn", out string updatedOn); + settings.TryGetValue("source", out string? source); + settings.TryGetValue("version", out string? version); + settings.TryGetValue("updatedOn", out string? updatedOn); m_subscriberInfo = $"{source.ToNonNullNorWhiteSpace("unknown source")} version {version.ToNonNullNorWhiteSpace("?.?.?.?")} updated on {updatedOn.ToNonNullNorWhiteSpace("undefined date")}"; } @@ -330,7 +322,7 @@ public bool Authenticated /// /// Gets active and standby keys and initialization vectors. /// - public byte[][][] KeyIVs => m_keyIVs; + public byte[][][]? KeyIVs => m_keyIVs; /// /// Gets current cipher index. @@ -349,17 +341,21 @@ public bool Authenticated /// This cache is for holding any updates while waiting for confirmation of /// receipt of signal index cache updates from the data subscriber. /// - public SignalIndexCache PendingSignalIndexCache { get; set; } + public SignalIndexCache? PendingSignalIndexCache { get; set; } /// /// Gets the lock object for updating Signal Index Cache properties. /// +#if NET + internal Lock PendingCacheUpdateLock { get; } +#else internal object PendingCacheUpdateLock { get; } +#endif /// /// Gets or sets the list of valid IP addresses that this client can connect from. /// - public List ValidIPAddresses { get; set; } + public List ValidIPAddresses { get; set; } = []; /// /// Gets the IP address of the remote client connection. @@ -369,7 +365,7 @@ public bool Authenticated /// /// Gets or sets subscription associated with this . /// - internal SubscriberAdapter Subscription + internal SubscriberAdapter? Subscription { get => m_subscription; set @@ -384,7 +380,7 @@ internal SubscriberAdapter Subscription /// /// Gets the subscriber name of this . /// - public string Name => SubscriberName; + public string Name => SubscriberName ?? "undefined"; /// /// Gets or sets a set of flags that define ways in @@ -426,11 +422,11 @@ public string Status status.AppendLine($" Subscriber name: {SubscriberName}"); status.AppendLine($" Subscriber acronym: {SubscriberAcronym}"); status.AppendLine($" Publish channel protocol: {ServerPublishChannel?.TransportProtocol.ToString() ?? "Not configured"}"); - status.AppendLine($" Data packet security: {(m_parent?.SecurityMode == SecurityMode.TLS && m_dataChannel is null ? "Secured via TLS" : m_keyIVs is null ? "Unencrypted" : "AES Encrypted")}"); + status.AppendLine($" Data packet security: {(m_parent.SecurityMode == SecurityMode.TLS && m_dataChannel is null ? "Secured via TLS" : m_keyIVs is null ? "Unencrypted" : "AES Encrypted")}"); status.AppendLine($" Current cache index: {CurrentCacheIndex}"); - status.AppendLine($"Signal index cache records: {SignalIndexCache?.Reference?.Count ?? 0:N0}"); + status.AppendLine($"Signal index cache records: {SignalIndexCache?.Reference.Count ?? 0:N0}"); - IServer serverCommandChannel = ServerCommandChannel; + IServer? serverCommandChannel = ServerCommandChannel; if (serverCommandChannel is not null) { @@ -438,7 +434,7 @@ public string Status status.Append(serverCommandChannel.Status); } - IClient clientCommandChannel = ClientCommandChannel; + IClient? clientCommandChannel = ClientCommandChannel; if (clientCommandChannel is not null) { @@ -456,7 +452,7 @@ public string Status } } - #endregion +#endregion #region [ Methods ] @@ -483,26 +479,17 @@ protected virtual void Dispose(bool disposing) if (!disposing) return; - if (m_pingTimer is not null) - { - m_pingTimer.Elapsed -= PingTimer_Elapsed; - m_pingTimer.Dispose(); - m_pingTimer = null; - } + m_pingTimer.Elapsed -= PingTimer_Elapsed; + m_pingTimer.Dispose(); - if (m_reconnectTimer is not null) - { - m_reconnectTimer.Elapsed -= ReconnectTimer_Elapsed; - m_reconnectTimer.Dispose(); - m_reconnectTimer = null; - } + m_reconnectTimer.Elapsed -= ReconnectTimer_Elapsed; + m_reconnectTimer.Dispose(); DataChannel = null; ServerCommandChannel = null; ClientCommandChannel = null; PublishBuffer.Dispose(); m_subscription = null; - m_parent = null; } finally { @@ -515,7 +502,11 @@ protected virtual void Dispose(bool disposing) /// internal void UpdateKeyIVs() { +#if NET + using (Aes symmetricAlgorithm = Aes.Create()) +#else using (AesManaged symmetricAlgorithm = new()) +#endif { symmetricAlgorithm.KeySize = 256; symmetricAlgorithm.GenerateKey(); @@ -581,7 +572,7 @@ public bool RotateCipherKeys() using (BlockAllocatedMemoryStream buffer = new()) { // Write even key - byte[] bufferLen = BigEndian.GetBytes(m_keyIVs[EvenKey][KeyIndex].Length); + byte[] bufferLen = BigEndian.GetBytes(m_keyIVs![EvenKey][KeyIndex].Length); buffer.Write(bufferLen, 0, bufferLen.Length); buffer.Write(m_keyIVs[EvenKey][KeyIndex], 0, m_keyIVs[EvenKey][KeyIndex].Length); @@ -638,45 +629,45 @@ public bool RotateCipherKeys() /// connection to send and receive data over the command channel. /// /// The socket instance used by the client to send and receive data over the command channel. - public Socket GetCommandChannelSocket() + public Socket? GetCommandChannelSocket() { return ServerCommandChannel switch { - TcpServer tcpServerCommandChannel when tcpServerCommandChannel.TryGetClient(ClientID, out TransportProvider tcpProvider) => tcpProvider.Provider, - TlsServer tlsServerCommandChannel when tlsServerCommandChannel.TryGetClient(ClientID, out TransportProvider tlsProvider) => tlsProvider.Provider?.Socket, - _ => (ClientCommandChannel as TcpClient)?.Client ?? (ClientCommandChannel as TcpSimpleClient)?.Client + TcpServer tcpServerCommandChannel when tcpServerCommandChannel.TryGetClient(ClientID, out TransportProvider? tcpProvider) => tcpProvider!.Provider, + TlsServer tlsServerCommandChannel when tlsServerCommandChannel.TryGetClient(ClientID, out TransportProvider? tlsProvider) => tlsProvider!.Provider?.Socket, + _ => (ClientCommandChannel as TcpClient)?.Client }; } // Send a no-op keep-alive ping to make sure the client is still connected - private void PingTimer_Elapsed(object sender, EventArgs e) + private void PingTimer_Elapsed(object? sender, EventArgs e) { m_parent.SendClientResponse(ClientID, ServerResponse.NoOP, ServerCommand.Subscribe); } - private void DataChannel_ClientConnectingException(object sender, EventArgs e) + private void DataChannel_ClientConnectingException(object? sender, EventArgs e) { Exception ex = e.Argument; m_parent.OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data channel exception occurred while sending client data to \"{m_connectionID}\": {ex.Message}", ex)); } - private void DataChannel_SendClientDataException(object sender, EventArgs e) + private void DataChannel_SendClientDataException(object? sender, EventArgs e) { Exception ex = e.Argument2; m_parent.OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data channel exception occurred while sending client data to \"{m_connectionID}\": {ex.Message}", ex)); } - private void DataChannel_ServerStarted(object sender, EventArgs e) + private void DataChannel_ServerStarted(object? sender, EventArgs e) { m_parent.OnStatusMessage(MessageLevel.Info, "Data channel started."); } - private void DataChannel_ServerStopped(object sender, EventArgs e) + private void DataChannel_ServerStopped(object? sender, EventArgs e) { if (m_connectionEstablished) { m_parent.OnStatusMessage(MessageLevel.Info, "Data channel stopped unexpectedly, restarting data channel..."); - m_reconnectTimer?.Start(); + m_reconnectTimer.Start(); } else { @@ -684,10 +675,13 @@ private void DataChannel_ServerStopped(object sender, EventArgs e) } } - private void ReconnectTimer_Elapsed(object sender, EventArgs e) + private void ReconnectTimer_Elapsed(object? sender, EventArgs e) { try { + if (m_configurationString is null) + throw new InvalidOperationException("Data channel configuration string is not defined."); + m_parent.OnStatusMessage(MessageLevel.Info, "Attempting to restart data channel..."); DataChannel = null; @@ -704,7 +698,7 @@ private void ReconnectTimer_Elapsed(object sender, EventArgs e) } } - #endregion +#endregion #region [ Static ] @@ -716,15 +710,15 @@ private void ReconnectTimer_Elapsed(object sender, EventArgs e) /// based client ID. /// Remote . /// End-user connection ID for an . - public static string GetEndPointConnectionID(Guid clientID, IPEndPoint remoteEndPoint) + public static string GetEndPointConnectionID(Guid clientID, IPEndPoint? remoteEndPoint) { - IPAddress ipAddress = IPAddress.None; - string hostName = null; - string connectionID = ""; + IPAddress? ipAddress = IPAddress.None; + string? hostName = null; + string? connectionID = ""; LookupEndPointInfo(clientID, remoteEndPoint, ref ipAddress, ref hostName, ref connectionID); - return connectionID; + return connectionID ?? ""; } /// @@ -735,7 +729,7 @@ public static string GetEndPointConnectionID(Guid clientID, IPEndPoint remoteEnd /// Parsed IP address. /// Looked-up DNS host name. /// String based connection identifier for human reference. - public static void LookupEndPointInfo(Guid clientID, IPEndPoint remoteEndPoint, ref IPAddress ipAddress, ref string hostName, ref string connectionID) + public static void LookupEndPointInfo(Guid clientID, IPEndPoint? remoteEndPoint, ref IPAddress? ipAddress, ref string? hostName, ref string? connectionID) { // Attempt to lookup remote connection identification for logging purposes try diff --git a/src/lib/SubscriberRightsLookup.cs b/src/lib/sttp.core/SubscriberRightsLookup.cs similarity index 88% rename from src/lib/SubscriberRightsLookup.cs rename to src/lib/sttp.core/SubscriberRightsLookup.cs index 81a7cf7f..961b7310 100644 --- a/src/lib/SubscriberRightsLookup.cs +++ b/src/lib/sttp.core/SubscriberRightsLookup.cs @@ -21,15 +21,6 @@ // //****************************************************************************************************** -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Text.RegularExpressions; -using GSF; -using GSF.Data; -using GSF.TimeSeries.Adapters; - namespace sttp; /// @@ -45,9 +36,9 @@ public class SubscriberRightsLookup /// /// The source of metadata providing the tables required by the rights logic. /// The ID of the subscriber whose rights are being looked up. - public SubscriberRightsLookup(DataSet dataSource, Guid subscriberID) + public SubscriberRightsLookup(DataSet? dataSource, Guid subscriberID) { - HasRightsFunc = BuildLookup(dataSource, subscriberID); + HasRightsFunc = dataSource is null ? _ => false : BuildLookup(dataSource, subscriberID); } #endregion @@ -85,7 +76,7 @@ private static Func BuildLookup(DataSet dataSource, Guid subscriberI // If subscriber has been disabled or removed // from the list of valid subscribers, // they no longer have rights to any signals - DataRow subscriber = dataSource.Tables["Subscribers"].Select($"ID = '{subscriberID}' AND Enabled <> 0").FirstOrDefault(); + DataRow? subscriber = dataSource.Tables["Subscribers"]?.Select($"ID = '{subscriberID}' AND Enabled <> 0").FirstOrDefault(); if (subscriber is null) return _ => false; @@ -93,10 +84,10 @@ private static Func BuildLookup(DataSet dataSource, Guid subscriberI //================================================================= // Check group implicitly authorized signals - DataRow[] subscriberMeasurementGroups = dataSource.Tables["SubscriberMeasurementGroups"].Select($"SubscriberID = '{subscriberID}'"); + DataRow[] subscriberMeasurementGroups = dataSource.Tables["SubscriberMeasurementGroups"]?.Select($"SubscriberID = '{subscriberID}'") ?? []; subscriberMeasurementGroups - .Join(dataSource.Tables["MeasurementGroups"].Select(), + .Join(dataSource.Tables["MeasurementGroups"]?.Select() ?? [], row => row.ConvertField("MeasurementGroupID"), row => row.ConvertField("ID"), (subscriberMeasurementGroup, measurementGroup) => @@ -116,9 +107,10 @@ private static Func BuildLookup(DataSet dataSource, Guid subscriberI //================================================================= // Check implicitly authorized signals - List matches = Regex.Matches(subscriber["AccessControlFilter"].ToNonNullString().ReplaceControlCharacters(), FilterRegex, RegexOptions.IgnoreCase) + // ReSharper disable once RedundantEnumerableCastCall + Match[] matches = Regex.Matches(subscriber["AccessControlFilter"].ToNonNullString().ReplaceControlCharacters(), FilterRegex, RegexOptions.IgnoreCase) .Cast() - .ToList(); + .ToArray(); // Combine individual allow statements into a single measurement filter string allowFilter = string.Join(" OR ", matches @@ -132,13 +124,13 @@ private static Func BuildLookup(DataSet dataSource, Guid subscriberI if (!string.IsNullOrEmpty(allowFilter)) { - foreach (DataRow row in dataSource.Tables["ActiveMeasurements"].Select(allowFilter)) + foreach (DataRow row in dataSource.Tables["ActiveMeasurements"]?.Select(allowFilter) ?? []) authorizedSignals.Add(row.ConvertField("SignalID")); } if (!string.IsNullOrEmpty(denyFilter)) { - foreach (DataRow row in dataSource.Tables["ActiveMeasurements"].Select(denyFilter)) + foreach (DataRow row in dataSource.Tables["ActiveMeasurements"]?.Select(denyFilter) ?? []) authorizedSignals.Remove(row.ConvertField("SignalID")); } @@ -146,7 +138,7 @@ private static Func BuildLookup(DataSet dataSource, Guid subscriberI // Check explicit group authorizations subscriberMeasurementGroups - .Join(dataSource.Tables["MeasurementGroupMeasurements"].Select(), + .Join(dataSource.Tables["MeasurementGroupMeasurements"]?.Select() ?? [], row => row.ConvertField("MeasurementGroupID"), row => row.ConvertField("MeasurementGroupID"), (subscriberMeasurementGroup, measurementGroupMeasurement) => new @@ -172,7 +164,7 @@ private static Func BuildLookup(DataSet dataSource, Guid subscriberI //=================================================================== // Check explicit authorizations - DataRow[] explicitAuthorizations = dataSource.Tables["SubscriberMeasurements"].Select($"SubscriberID = '{subscriberID}'"); + DataRow[] explicitAuthorizations = dataSource.Tables["SubscriberMeasurements"]?.Select($"SubscriberID = '{subscriberID}'") ?? []; // Add all explicitly authorized signals to authorizedSignals foreach (DataRow explicitAuthorization in explicitAuthorizations) diff --git a/src/lib/SubscriptionInfo.cs b/src/lib/sttp.core/SubscriptionInfo.cs similarity index 97% rename from src/lib/SubscriptionInfo.cs rename to src/lib/sttp.core/SubscriptionInfo.cs index 1de69649..f8bd9be9 100644 --- a/src/lib/SubscriptionInfo.cs +++ b/src/lib/sttp.core/SubscriptionInfo.cs @@ -23,9 +23,6 @@ // //****************************************************************************************************** -using GSF; -using System; - namespace sttp; /// @@ -51,7 +48,7 @@ public SubscriptionInfo(bool throttled = false) /// Gets or sets the filter expression used to define which /// measurements are being requested by the subscriber. /// - public string FilterExpression { get; set; } + public string FilterExpression { get; set; } = null!; /// /// Gets or sets the flag that determines whether to use the @@ -154,7 +151,7 @@ public SubscriptionInfo(bool throttled = false) /// /// /// - public string StartTime { get; set; } + public string? StartTime { get; set; } /// /// Gets or sets the stop time of the requested @@ -202,13 +199,13 @@ public SubscriptionInfo(bool throttled = false) /// /// /// - public string StopTime { get; set; } + public string? StopTime { get; set; } /// /// Gets or sets the additional constraint parameters /// supplied to temporal adapters in a temporal session. /// - public string ConstraintParameters { get; set; } + public string? ConstraintParameters { get; set; } /// /// Gets or sets the processing interval requested by the subscriber. @@ -247,7 +244,7 @@ public SubscriptionInfo(bool throttled = false) /// be applied to the connection string sent to the publisher /// during subscription. /// - public string ExtraConnectionStringParameters { get; set; } + public string? ExtraConnectionStringParameters { get; set; } #endregion } \ No newline at end of file diff --git a/src/lib/sttp.core/sttp.core.projitems b/src/lib/sttp.core/sttp.core.projitems new file mode 100644 index 00000000..81bdb165 --- /dev/null +++ b/src/lib/sttp.core/sttp.core.projitems @@ -0,0 +1,27 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 5a705fbb-8702-4628-bd2e-9b04e4f7f517 + + + sttp.core + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/lib/sttp.core/sttp.core.shproj b/src/lib/sttp.core/sttp.core.shproj new file mode 100644 index 00000000..63dfccc3 --- /dev/null +++ b/src/lib/sttp.core/sttp.core.shproj @@ -0,0 +1,13 @@ + + + + 5a705fbb-8702-4628-bd2e-9b04e4f7f517 + 14.0 + + + + + + + + diff --git a/src/lib/tssc/TsscCodeWords.cs b/src/lib/sttp.core/tssc/TsscCodeWords.cs similarity index 100% rename from src/lib/tssc/TsscCodeWords.cs rename to src/lib/sttp.core/tssc/TsscCodeWords.cs diff --git a/src/lib/tssc/TsscDecoder.cs b/src/lib/sttp.core/tssc/TsscDecoder.cs similarity index 97% rename from src/lib/tssc/TsscDecoder.cs rename to src/lib/sttp.core/tssc/TsscDecoder.cs index 2d8863de..e8aaec64 100644 --- a/src/lib/tssc/TsscDecoder.cs +++ b/src/lib/sttp.core/tssc/TsscDecoder.cs @@ -21,10 +21,6 @@ // //****************************************************************************************************** -using System; -using GSF; -using GSF.Collections; - namespace sttp.tssc; /// @@ -32,7 +28,7 @@ namespace sttp.tssc; /// public class TsscDecoder { - private byte[] m_data; + private byte[] m_data = null!; private int m_position; private int m_lastPosition; @@ -44,8 +40,8 @@ public class TsscDecoder private long m_prevTimeDelta3; private long m_prevTimeDelta4; - private TsscPointMetadata m_lastPoint; - private IndexedArray m_points; + private TsscPointMetadata m_lastPoint = null!; + private IndexedArray m_points = null!; internal ushort SequenceNumber; @@ -68,8 +64,8 @@ public TsscDecoder() /// public void Reset() { - m_points = new IndexedArray(); - m_lastPoint = new TsscPointMetadata(null, ReadBit, ReadBits5); + m_points = new IndexedArray(); + m_lastPoint = new TsscPointMetadata(null!, ReadBit, ReadBits5); m_data = []; m_position = 0; m_lastPosition = 0; @@ -153,11 +149,11 @@ public unsafe bool TryGetMeasurement(out int id, out long timestamp, out uint qu id = m_lastPoint.PrevNextPointId1; - TsscPointMetadata nextPoint = m_points[m_lastPoint.PrevNextPointId1]; + TsscPointMetadata? nextPoint = m_points[m_lastPoint.PrevNextPointId1]; if (nextPoint is null) { - nextPoint = new TsscPointMetadata(null, ReadBit, ReadBits5); + nextPoint = new TsscPointMetadata(null!, ReadBit, ReadBits5); m_points[id] = nextPoint; nextPoint.PrevNextPointId1 = id + 1; } diff --git a/src/lib/tssc/TsscEncoder.cs b/src/lib/sttp.core/tssc/TsscEncoder.cs similarity index 97% rename from src/lib/tssc/TsscEncoder.cs rename to src/lib/sttp.core/tssc/TsscEncoder.cs index 588789f0..8c04355a 100644 --- a/src/lib/tssc/TsscEncoder.cs +++ b/src/lib/sttp.core/tssc/TsscEncoder.cs @@ -22,10 +22,7 @@ //****************************************************************************************************** // ReSharper disable IntVariableOverflowInUncheckedContext // ReSharper disable UnusedMember.Local - -using System; -using GSF; -using GSF.Collections; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. namespace sttp.tssc; @@ -56,7 +53,7 @@ public class TsscEncoder private long m_prevTimeDelta4; private TsscPointMetadata m_lastPoint; - private IndexedArray m_points; + private IndexedArray m_points; /// /// Creates an encoder for the TSSC protocol. @@ -77,8 +74,8 @@ public TsscEncoder() /// public void Reset() { - m_points = new IndexedArray(); - m_lastPoint = new TsscPointMetadata(WriteBits, null, null); + m_points = new IndexedArray(); + m_lastPoint = new TsscPointMetadata(WriteBits, null!, null!); m_data = []; m_position = 0; m_lastPosition = 0; @@ -133,11 +130,11 @@ public unsafe bool TryAddMeasurement(int id, long timestamp, uint quality, float if (m_lastPosition - m_position < 100) return false; - TsscPointMetadata point = m_points[id]; + TsscPointMetadata? point = m_points[id]; if (point is null) { - point = new TsscPointMetadata(WriteBits, null, null) { PrevNextPointId1 = id + 1 }; + point = new TsscPointMetadata(WriteBits, null!, null!) { PrevNextPointId1 = id + 1 }; m_points[id] = point; } diff --git a/src/lib/tssc/TsscPointMetadata.cs b/src/lib/sttp.core/tssc/TsscPointMetadata.cs similarity index 98% rename from src/lib/tssc/TsscPointMetadata.cs rename to src/lib/sttp.core/tssc/TsscPointMetadata.cs index 4b49bc00..abe774ca 100644 --- a/src/lib/tssc/TsscPointMetadata.cs +++ b/src/lib/sttp.core/tssc/TsscPointMetadata.cs @@ -20,9 +20,7 @@ // Generated original version of source code. // //****************************************************************************************************** - - -using System; +// ReSharper disable NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract namespace sttp.tssc; diff --git a/src/lib/sttp.gemstone/GlobalUsings.cs b/src/lib/sttp.gemstone/GlobalUsings.cs new file mode 100644 index 00000000..c7e0292c --- /dev/null +++ b/src/lib/sttp.gemstone/GlobalUsings.cs @@ -0,0 +1,48 @@ +global using System; +global using System.Collections.Concurrent; +global using System.ComponentModel; +global using System.Configuration; +global using System.Data; +global using System.Data.Common; +global using System.Globalization; +global using System.IO.Compression; +global using System.Net; +global using System.Net.Security; +global using System.Net.Sockets; +global using System.Security.Cryptography; +global using System.Security.Cryptography.X509Certificates; +global using System.Text; +global using System.Text.RegularExpressions; +global using System.Xml; +global using Gemstone; +global using Gemstone.ActionExtensions; +global using Gemstone.ArrayExtensions; +global using Gemstone.Collections; +global using Gemstone.Collections.CollectionExtensions; +global using Gemstone.Communication; +global using Gemstone.ComponentModel.DataAnnotations; +global using Gemstone.Configuration; +global using Gemstone.Data; +global using Gemstone.Data.DataExtensions; +global using Gemstone.Diagnostics; +global using Gemstone.EventHandlerExtensions; +global using Gemstone.GuidExtensions; +global using Gemstone.IO; +global using Gemstone.IO.Parsing; +global using Gemstone.IO.StreamExtensions; +global using Gemstone.Net.Security; +global using Gemstone.Reflection; +global using Gemstone.Security.Cryptography.SymmetricAlgorithmExtensions; +global using Gemstone.StringExtensions; +global using Gemstone.Timeseries; +global using Gemstone.Timeseries.Adapters; +global using Gemstone.Timeseries.Data; +global using Gemstone.Timeseries.Statistics; +global using Gemstone.Timeseries.Transport; +global using Gemstone.Threading; +global using Gemstone.Threading.SynchronizedOperations; +global using Gemstone.Units; +global using TcpClient = Gemstone.Communication.TcpClient; +global using UdpClient = Gemstone.Communication.UdpClient; +global using ConfigSettings = Gemstone.Configuration.Settings; +global using ProtocolType = Gemstone.Timeseries.Adapters.ProtocolType; diff --git a/src/lib/sttp.gemstone/UI/STTP.js b/src/lib/sttp.gemstone/UI/STTP.js new file mode 100644 index 00000000..973e74e0 --- /dev/null +++ b/src/lib/sttp.gemstone/UI/STTP.js @@ -0,0 +1,2 @@ +/*! For license information please see STTP.js.LICENSE.txt */ +var protocol_ui;(()=>{var e,t={93195:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t},i=this&&this.__spreadArray||function(e,t,n){if(n||2===arguments.length)for(var o,r=0,a=t.length;re.Pipelines.length-1||e.CurrentPipelineStep>(null===(o=null===(n=null===(t=e.Pipelines)||void 0===t?void 0:t[v])||void 0===n?void 0:n.Steps)||void 0===o?void 0:o.length)-1)return c.createElement(c.Fragment,null);var p=e.Pipelines[v].Steps[e.CurrentPipelineStep];return c.createElement(p.UI,{AdditionalProps:null===(i=e.Pipelines[v].Steps[e.CurrentPipelineStep])||void 0===i?void 0:i.AdditionalProps,RawFileData:A,SetData:a,Data:r,SetPipelineStep:e.SetCurrentPipelineStep,CurrentPipelineStep:e.CurrentPipelineStep,Errors:M,SetErrors:d})},[e.Step,v,A,e.CurrentPipelineStep,e.Pipelines]),x=c.useMemo(function(){var t,n,o;if(null!=e.ProgressBar)return[];if(null==v||v>e.Pipelines.length-1||e.CurrentPipelineStep>(null===(o=null===(n=null===(t=e.Pipelines)||void 0===t?void 0:t[v])||void 0===n?void 0:n.Steps)||void 0===o?void 0:o.length)-1)return s;var r=e.Pipelines[v].Steps.map(function(e,t){return{short:e.Label,long:e.Label,id:t}}),a=s.findIndex(function(e){return"Upload"===e.short}),i=s.findIndex(function(e){return"Process"===e.short}),c=s.slice(i+1);return[s[a]].concat(r,c)},[v,e.Pipelines]);return c.useEffect(function(){var t,n,o;"Process"===e.Step?null==v||v>e.Pipelines.length-1||e.CurrentPipelineStep>(null===(o=null===(n=null===(t=e.Pipelines)||void 0===t?void 0:t[v])||void 0===n?void 0:n.Steps)||void 0===o?void 0:o.length)-1||w(e.CurrentPipelineStep):w(e.Step)},[e.CurrentPipelineStep,v,e.Step]),c.useEffect(function(){var t="Process"==e.Step?M:[],n=i([],t,!0);"Upload"===e.Step&&(null==O&&n.push("A file must be uploaded to continue"),null!=A&&""!=A||n.push("File content is empty"),g||n.push("File must be of type ".concat(e.FileTypeAttribute))),e.SetErrors(n)},[A,O,g,M,e.Step]),c.useEffect(function(){var t;("Review"===e.Step&&null!==(t=e.CompleteOnReview)&&void 0!==t&&t||"Complete"===e.Step)&&e.OnComplete(r)},[e.Step,e.CompleteOnReview,r]),c.createElement("div",{className:"container-fluid d-flex flex-column p-0 h-100"},c.createElement("div",{className:"row h-100"},c.createElement("div",{className:"col-12 d-flex flex-column h-100"},c.createElement("div",{className:"row"},c.createElement("div",{className:"col-12"},null!=e.ProgressBar?e.ProgressBar:c.createElement(p.ProgressBar,{steps:x,activeStep:R}))),"Upload"===e.Step?c.createElement(c.Fragment,null,c.createElement("div",{className:"row justify-content-center"},c.createElement("div",{className:"col-6"},c.createElement(l.FileUpload,{OnLoadHandler:function(t){return new Promise(function(n,o){try{if(null==t)return void o();var r=t.name.match(u),a=null!=r?r[0].substring(1):"",i=e.Pipelines.findIndex(function(e){return e.Select(t.type,a)});if(-1==i)return W(!1),void o();_(i),f(t.name);var c=new FileReader;c.readAsText(t),c.onload=function(e){null!=e.target?(m(e.target.result),n()):o()},c.onerror=function(){return o()}}catch(e){o()}})},OnClearHandler:function(){W(!0),_(null),f(null),m(null)},FileTypeAttribute:e.FileTypeAttribute}))),c.createElement("div",{className:"row"},c.createElement("div",{className:"col-12 h-100"},null!=v&&null!=(null===(t=e.Pipelines[v])||void 0===t?void 0:t.AdditionalUploadUI)?null===(n=e.Pipelines[v])||void 0===n?void 0:n.AdditionalUploadUI:null))):null,c.createElement("div",{className:"".concat("Process"!==e.Step?"d-none":"row flex-grow-1"),style:{overflowY:"hidden"}},c.createElement("div",{className:"col-12 h-100"},S)),"Review"===e.Step||"Complete"===e.Step&&null!=e.CompleteUI?c.createElement("div",{className:"row flex-grow-1",style:{overflowY:"hidden"}},c.createElement("div",{className:"col-12 h-100"},"Review"==e.Step?c.createElement(e.ReviewUI,{Data:r}):"Complete"===e.Step?e.CompleteUI:null)):null)))};var c=a(n(8674)),p=n(68976),l=n(90782),s=[{short:"Upload",long:"Upload",id:"Upload"},{short:"Process",long:"Process",id:"Process"},{short:"Review",id:"Review",long:"Review"},{short:"Complete",long:"Complete",id:"Complete"}],u=/(\.[^.]+)$/},49808:function(e,t,n){"use strict";var o,r=n(4364),a=this&&this.__extends||(o=function(e,t){return o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},o(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}o(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),i=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),c=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),p=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&i(t,e,n);return c(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var l=p(n(8674)),s=n(68976),u=function(e){function t(t){var n=e.call(this,t)||this;return n.state={name:"",message:""},n}return a(t,e),t.prototype.componentDidCatch=function(e){this.setState({name:e.name,message:e.message}),r.warn(e)},t.prototype.render=function(){var e;return this.state.name.length>0?l.createElement("div",{className:this.props.ClassName,style:this.props.Style},l.createElement(s.ServerErrorIcon,{Show:!0,Label:this.props.ErrorMessage,Size:null!==(e=this.props.ErrorIconSize)&&void 0!==e?e:150})):l.createElement(l.Fragment,null,this.props.children)},t}(l.Component);t.default=u},15903:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n=0&&t.durationMin<100&&(u(t.durationMax)||t.durationMax>=t.durationMin):"durationMax"==e?u(t.durationMax)||t.durationMax>=0&&t.durationMax<100&&(u(t.durationMin)||t.durationMax>=t.durationMin):"sagMin"==e?u(t.sagMin)||t.sagMin>=0&&t.sagMin<1&&(u(t.sagMax)||t.sagMax>=t.sagMin):"sagMax"==e?u(t.sagMax)||t.sagMax>=0&&t.sagMax<1&&(u(t.sagMax)||t.sagMax>=t.sagMax):"swellMin"==e?u(t.swellMin)||t.swellMin>=1&&t.swellMin<9999&&(u(t.swellMax)||t.swellMax>=t.swellMin):"swellMax"==e?u(t.swellMax)||t.swellMax>=1&&t.swellMax<9999&&(u(t.swellMin)||t.swellMax>=t.swellMin):"transientMin"==e?u(t.transientMin)||t.transientMin>=0&&t.transientMin<9999&&(u(t.transientMax)||t.transientMax>=t.transientMin):"transientMax"!=e||u(t.transientMax)||t.transientMax>=0&&t.transientMax<9999&&(u(t.transientMin)||t.transientMax>=t.transientMin)}a.default.useEffect(function(){r(e.eventCharacteristicFilter)},[e.eventCharacteristicFilter]),a.default.useEffect(function(){var t=[];Object.keys(e.eventCharacteristicFilter.phases).forEach(function(n,o){return t.push({Value:o,Label:n,Selected:e.eventCharacteristicFilter.phases[n]})}),s(t)},[]),a.default.useEffect(function(){var t,o=((t=null!=n)?t=(t=(t=(t=(t=(t=(t=(t=t&&b("durationMin"))&&b("durationMax"))&&b("sagMin"))&&b("sagMax"))&&b("swellMin"))&&b("swellMax"))&&b("transientMin"))&&b("transientMax"):t)?n:void 0;i.default.isEqual(o,e.eventCharacteristicFilter)||e.setEventFilters(o)},[n]);var M=null!=e.eventTypeFilter.find(function(t){var n;return t==(null===(n=e.eventTypes.find(function(e){return"Sag"==e.Name}))||void 0===n?void 0:n.ID)}),d=null!=e.eventTypeFilter.find(function(t){var n;return t==(null===(n=e.eventTypes.find(function(e){return"Swell"==e.Name}))||void 0===n?void 0:n.ID)}),z=null!=e.eventTypeFilter.find(function(t){var n;return t==(null===(n=e.eventTypes.find(function(e){return"Transient"==e.Name}))||void 0===n?void 0:n.ID)});return null===n||null===e.eventTypeFilter?null:a.default.createElement("fieldset",{className:"border",style:{padding:"10px",height:"100%"}},a.default.createElement("legend",{className:"w-auto",style:{fontSize:"large"}},"Event Characteristics:"),a.default.createElement("div",{className:"row"},a.default.createElement("div",{className:"col-4"},a.default.createElement("form",null,a.default.createElement("div",{className:"form-group"},a.default.createElement("div",{className:"input-group input-group-sm",style:{width:"100%"}},a.default.createElement(c.Select,{Record:n,Label:"Mag-Dur:",Field:"curveID",Setter:r,Options:e.magDurCurves.map(function(e){return{Value:e.ID.toString(),Label:e.Name}})})),a.default.createElement("div",{className:"form-check form-check-inline"},a.default.createElement(c.RadioButtons,{Record:n,Label:"",Field:"sagType",Setter:r,Options:[{Value:"LL",Label:"LL"},{Value:"LN",Label:"LN"},{Value:"both",Label:"Both"}]}))))),a.default.createElement("div",{className:"col-4"},a.default.createElement("form",null,a.default.createElement("label",{style:{margin:0}},"Duration (cycle):"),a.default.createElement("div",{className:"form-group"},a.default.createElement("div",{className:"input-group input-group-sm"},a.default.createElement("div",{className:"col",style:{width:"45%",paddingRight:0,paddingLeft:0}},a.default.createElement(c.Input,{Record:n,Label:"",Field:"durationMin",Setter:r,Valid:b,Feedback:"Invalid Min",Type:"number",Size:"small",AllowNull:!0})),a.default.createElement("div",{className:"input-group-append",style:{height:"37px"}},a.default.createElement("span",{className:"input-group-text"}," to ")),a.default.createElement("div",{className:"col",style:{width:"45%",paddingLeft:0,paddingRight:0}},a.default.createElement(c.Input,{Record:n,Label:"",Field:"durationMax",Setter:r,Valid:b,Feedback:"Invalid Max",Type:"number",Size:"small",AllowNull:!0})))))),a.default.createElement("div",{className:"col-4"},a.default.createElement("form",null,a.default.createElement("label",{style:{margin:0}},"Sags (p.u.):"),a.default.createElement("div",{className:"form-group"},a.default.createElement("div",{className:"row",style:{width:"100%"}},a.default.createElement("div",{className:"input-group input-group-sm"},a.default.createElement("div",{className:"col",style:{width:"45%",paddingLeft:0,paddingRight:0}},a.default.createElement(c.Input,{Record:n,Label:"",Disabled:!M,Field:"sagMin",Setter:r,Valid:b,Feedback:"Invalid Min",Type:"number",Size:"small",AllowNull:!0})),a.default.createElement("div",{className:"input-group-append",style:{height:"37px"}},a.default.createElement("span",{className:"input-group-text"}," to ")),a.default.createElement("div",{className:"col",style:{width:"45%",paddingLeft:0,paddingRight:0}},a.default.createElement(c.Input,{Record:n,Label:"",Disabled:!M,Field:"sagMax",Setter:r,Valid:b,Feedback:"Invalid Max",Type:"number",Size:"small",AllowNull:!0})))),a.default.createElement("div",{className:"row justify-content-md-center"},a.default.createElement("div",{className:"form-check form-check-inline"},a.default.createElement(c.RadioButtons,{Record:n,Label:"",Field:"sagType",Setter:r,Options:[{Value:"LL",Label:"LL"},{Value:"LN",Label:"LN"},{Value:"both",Label:"Both"}]})))))),a.default.createElement("div",{className:"col-4"},a.default.createElement(c.MultiCheckBoxSelect,{Options:l,Label:"Phases",ShowToolTip:!0,OnChange:function(e,t){var a=[],i=o({},n.phases);l.forEach(function(e){var n=e.Selected!=t.findIndex(function(t){return e.Value===t.Value})>-1;a.push(o(o({},e),{Selected:n})),i[e.Label]=n}),s(a),r(o(o({},n),{phases:i}))}})),a.default.createElement("div",{className:"col-4"},a.default.createElement("form",null,a.default.createElement("label",{style:{margin:0}},"Transients (p.u.):"),a.default.createElement("div",{className:"form-group"},a.default.createElement("div",{className:"input-group input-group-sm"},a.default.createElement("div",{className:"row",style:{width:"100%"}},a.default.createElement("div",{className:"col",style:{width:"45%",paddingLeft:0,paddingRight:0}},a.default.createElement(c.Input,{Record:n,Label:"",Disabled:!z,Field:"transientMin",Setter:r,Valid:b,Feedback:"Invalid Min",Type:"number",Size:"small",AllowNull:!0})),a.default.createElement("div",{className:"input-group-append",style:{height:"37px"}},a.default.createElement("span",{className:"input-group-text"}," to ")),a.default.createElement("div",{className:"col",style:{width:"45%",paddingLeft:0,paddingRight:0}},a.default.createElement(c.Input,{Record:n,Label:"",Disabled:!z,Field:"transientMax",Setter:r,Valid:b,Feedback:"Invalid Max",Size:"small",AllowNull:!0,Type:"number"})))),a.default.createElement("div",{className:"row justify-content-md-center"},a.default.createElement("div",{className:"form-check form-check-inline"},a.default.createElement(c.RadioButtons,{Record:n,Label:"",Field:"sagType",Setter:r,Options:[{Value:"LL",Label:"LL"},{Value:"LN",Label:"LN"},{Value:"both",Label:"Both"}]})))))),a.default.createElement("div",{className:"col-4"},a.default.createElement("form",null,a.default.createElement("label",{style:{margin:0}},"Swells (p.u.):"),a.default.createElement("div",{className:"form-group"},a.default.createElement("div",{className:"row",style:{width:"100%"}},a.default.createElement("div",{className:"input-group input-group-sm"},a.default.createElement("div",{className:"col",style:{width:"45%",paddingLeft:0,paddingRight:0}},a.default.createElement(c.Input,{Record:n,Label:"",Disabled:!d,Field:"swellMin",Setter:r,Valid:b,Feedback:"Invalid Min",Type:"number",Size:"small",AllowNull:!0})),a.default.createElement("div",{className:"input-group-append",style:{height:"37px"}},a.default.createElement("span",{className:"input-group-text"}," to ")),a.default.createElement("div",{className:"col",style:{width:"45%",paddingLeft:0,paddingRight:0}},a.default.createElement(c.Input,{Record:n,Label:"",Disabled:!d,Field:"swellMax",Setter:r,Valid:b,Feedback:"Invalid Max",Type:"number",Size:"small",AllowNull:!0})))),a.default.createElement("div",{className:"row justify-content-md-center"},a.default.createElement("div",{className:"form-check form-check-inline"},a.default.createElement(c.RadioButtons,{Record:n,Label:"",Field:"sagType",Setter:r,Options:[{Value:"LL",Label:"LL"},{Value:"LN",Label:"LN"},{Value:"both",Label:"Both"}]}))))))))}},27034:function(e,t,n){"use strict";var o=this&&this.__spreadArray||function(e,t,n){if(n||2===arguments.length)for(var o,r=0,a=t.length;r0?e.Label:"Other Types",":",a.default.createElement("a",{style:{fontSize:"small",color:"#0056b3",marginLeft:2,cursor:"pointer"},onClick:function(){var t=0==e.Data.filter(function(t){return null==e.SelectedID.find(function(e){return e==t.ID})}).length;e.SelectAll(t)}},"(",0==e.Data.filter(function(t){return null==e.SelectedID.find(function(e){return e==t.ID})}).length?"un":"","select all)")),a.default.createElement("form",null,a.default.createElement("ul",{style:{listStyleType:"none",padding:0,position:"relative",float:"left"}},e.Data.map(function(t){return a.default.createElement("li",{key:t.ID},a.default.createElement("label",null,a.default.createElement("input",{type:"checkbox",onChange:function(n){e.OnChange(t,n.target.checked)},checked:null!=e.SelectedID.find(function(e){return e==t.ID})}),t.Description))}))))};t.default=function(e){var t=a.default.useState([]),n=t[0],r=t[1],p=a.default.useState(1),l=p[0],s=p[1];return a.default.useEffect(function(){return r(i.default.uniq(e.EventTypes.map(function(e){return e.Category})).map(function(e){return{label:null!=e?e:"",height:0}}))},[e.EventTypes]),a.default.useEffect(function(){if(null!=e.Height){var t=e.Height,o=n.map(function(e){return e.height});o.some(function(e){return e>t})&&(t=Math.max.apply(Math,o));var r=0;o.sort();for(var a=function(){r+=1;var e=o[0];o.splice(0,1);for(var n=o.findIndex(function(n){return n<=t-e});n>=0&&0!=o[n];)e+=o[n],o.splice(n,1),n=o.findIndex(function(n){return n<=t-e})};o.length>0;)a();s(r)}else s(1)},[n,e.Height]),a.default.createElement("ul",{className:"navbar-nav mr-auto",style:{width:"100%"}},Array.from({length:l},function(e,t){return t}).map(function(t){return function(t){var p=[];if(null!=e.Height){var s=e.Height;n.some(function(e){return e.height>s})&&(s=Math.max.apply(Math,n.map(function(e){return e.height})));for(var u=i.default.orderBy(n,function(e){return e.height}),b=0,M=function(){(b+=1)==t+1&&p.push(u[0]);var e=u[0].height;u.splice(0,1);for(var n=u.findIndex(function(t){return t.height<=s-e});n>=0;)e+=u[n].height,b==t+1&&p.push(u[n]),u.splice(n,1),n=u.findIndex(function(t){return t.height<=s-e})};u.length>0&&b<=t;)M()}else p=n;return a.default.createElement("li",{className:"nav-item",style:{width:(100/l).toFixed(0)+"%",height:n.some(function(e){return 0==e.height})?5:"100%",overflow:"hidden"}},p.map(function(t){return a.default.createElement(c,{key:t.label,Label:t.label,SelectedID:e.SelectedTypeID,SelectAll:function(n){e.SetSelectedTypeIDs(n?e.SelectedTypeID.filter(function(n){return null==e.EventTypes.find(function(e){var o;return n==e.ID&&(null!==(o=e.Category)&&void 0!==o?o:"")==t.label})}):i.default.uniq(o(o([],e.SelectedTypeID,!0),e.EventTypes.filter(function(e){var n;return(null!==(n=e.Category)&&void 0!==n?n:"")==t.label}).map(function(e){return e.ID}),!0)))},Data:e.EventTypes.filter(function(e){var n;return(null!==(n=e.Category)&&void 0!==n?n:"")==t.label}),OnChange:function(t,n){e.SetSelectedTypeIDs(n?o(o([],e.SelectedTypeID,!0),[t.ID],!1):e.SelectedTypeID.filter(function(e){return e!=t.ID}))},SetHeight:function(e){return function(e,t){var o=n.findIndex(function(t){return t.label==e});o>-1&&n[o].height!=t&&r(function(e){var n=i.default.cloneDeep(e);return n[o].height=t,n})}(t.label,e)}})}))}(t)}))}},88172:function(e,t,n){"use strict";var o,r=this&&this.__extends||(o=function(e,t){return o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},o(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}o(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.ReadWriteControllerFunctions=t.ReadOnlyControllerFunctions=void 0;var i=a(n(34651)),c=function(e){var t=this;this.GetPageInfo=function(e,n){var o=null==n?"".concat(t.APIPath,"/PageInfo"):"".concat(t.APIPath,"/PageInfo/").concat(n);return void 0===e||0===e.length?i.default.ajax({type:"GET",url:"".concat(o),contentType:"application/json; charset=utf-8",dataType:"json",cache:!1,async:!0}):i.default.ajax({type:"POST",url:"".concat(o),contentType:"application/json; charset=utf-8",dataType:"json",data:JSON.stringify({Searches:e,OrderBy:"",Ascending:!1}),cache:!1,async:!0})},this.GetOne=function(e){return i.default.ajax({type:"GET",url:"".concat(t.APIPath,"/One/").concat(e),contentType:"application/json; charset=utf-8",dataType:"json",cache:!1,async:!0})},this.GetPage=function(e,n,o,r){return null==n||null==o?null!=r?i.default.ajax({type:"GET",url:"".concat(t.APIPath,"/").concat(e,"/").concat(r),contentType:"application/json; charset=utf-8",dataType:"json",cache:!1,async:!0}):i.default.ajax({type:"GET",url:"".concat(t.APIPath,"/").concat(e),contentType:"application/json; charset=utf-8",dataType:"json",cache:!1,async:!0}):null!=r?i.default.ajax({type:"GET",url:"".concat(t.APIPath,"/").concat(e,"/").concat(r,"/").concat(n.toString(),"/").concat(o),contentType:"application/json; charset=utf-8",dataType:"json",cache:!1,async:!0}):i.default.ajax({type:"GET",url:"".concat(t.APIPath,"/").concat(e,"/").concat(n.toString(),"/").concat(o),contentType:"application/json; charset=utf-8",dataType:"json",cache:!1,async:!0})},this.GetAll=function(e,n,o,r){var a=i.default.Deferred(),c=[],p=t.GetPageInfo(null!=o?o:[],r).done(function(p){var l=p.PageCount;if(l<=0)a.resolve([]);else{for(var s=[],u=0;u{"use strict";var n;Object.defineProperty(t,"__esModule",{value:!0}),t.Gemstone=void 0,function(e){(e.HelperFunctions||(e.HelperFunctions={})).getSearchFilter=function(e){return e.map(function(e){var t,n=function(t){switch(e.Type){case"number":case"integer":return Number(t);case"boolean":return"1"===t;default:return t}},o=e.SearchText;return"IN"!==e.Operator&&"NOT IN"!==e.Operator||o.startsWith("(")&&o.endsWith(")")&&(o=o.slice(1,-1).split(",").map(function(e){return e.trim()})),t=Array.isArray(o)?o.map(function(e){return n(e)}):n(o),{FieldName:e.FieldName,SearchParameter:t,Operator:e.Operator}})}}(n||(t.Gemstone=n={}))},35037:function(e,t,n){"use strict";var o=this&&this.__awaiter||function(e,t,n,o){return new(n||(n=Promise))(function(r,a){function i(e){try{p(o.next(e))}catch(e){a(e)}}function c(e){try{p(o.throw(e))}catch(e){a(e)}}function p(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(i,c)}p((o=o.apply(e,t||[])).next())})},r=this&&this.__generator||function(e,t){var n,o,r,a,i={label:0,sent:function(){if(1&r[0])throw r[1];return r[1]},trys:[],ops:[]};return a={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function c(c){return function(p){return function(c){if(n)throw new TypeError("Generator is already executing.");for(;a&&(a=0,c[0]&&(i=0)),i;)try{if(n=1,o&&(r=2&c[0]?o.return:c[0]?o.throw||((r=o.return)&&r.call(o),0):o.next)&&!(r=r.call(o,c[1])).done)return r;switch(o=0,r&&(c=[2&c[0],r.value]),c[0]){case 0:case 1:r=c;break;case 4:return i.label++,{value:c[1],done:!1};case 5:i.label++,o=c[1],c=[0];continue;case 7:c=i.ops.pop(),i.trys.pop();continue;default:if(!((r=(r=i.trys).length>0&&r[r.length-1])||6!==c[0]&&2!==c[0])){i=0;continue}if(3===c[0]&&(!r||c[1]>r[0]&&c[1]0||(e.FetchStatus="error",e.Error={Message:null!==(n=t.error.message)&&void 0!==n?n:"",Action:"FETCH",Time:(new Date).toString()})}),e.addCase(n.Fetch.fulfilled,function(e,t){var n,o;e.ActiveID=e.ActiveID.filter(function(e){return e!==t.meta.requestId}),e.FetchStatus="idle",e.Error=null,e.Data=t.payload.Data,null!=t.meta.arg&&(null!=t.meta.arg.filter&&(e.Filter=null===(n=t.meta.arg)||void 0===n?void 0:n.filter),null!=t.meta.arg.parentID&&(e.ParentID=null===(o=t.meta.arg)||void 0===o?void 0:o.parentID),e.SortField===t.meta.arg.sortField?e.Ascending=!e.Ascending:null!=t.meta.arg.sortField&&(e.SortField=t.meta.arg.sortField))}),e.addCase(n.SetStatusToChanged.pending,function(e){e.FetchStatus="changed"})}}),this.Reducer=this.Slice.reducer},e.prototype.addExtraReducers=function(e){e.addCase(this.Fetch.pending,function(e,t){e.FetchStatus="loading",e.ActiveID.push(t.meta.requestId)}),e.addCase(this.Fetch.rejected,function(e,t){var n;e.ActiveID=e.ActiveID.filter(function(e){return e!==t.meta.requestId}),e.ActiveID.length>0||(e.FetchStatus="error",e.Error={Message:null!==(n=t.error.message)&&void 0!==n?n:"",Action:"FETCH",Time:(new Date).toString()})}),e.addCase(this.Fetch.fulfilled,function(e,t){var n,o;e.ActiveID=e.ActiveID.filter(function(e){return e!==t.meta.requestId}),e.FetchStatus="idle",e.Error=null,e.Data=t.payload.Data,null!=t.meta.arg.filter&&(e.Filter=null===(n=t.meta.arg)||void 0===n?void 0:n.filter),null!=t.meta.arg.parentID&&(e.ParentID=null===(o=t.meta.arg)||void 0===o?void 0:o.parentID),e.SortField===t.meta.arg.sortField?e.Ascending=!e.Ascending:null!=t.meta.arg.sortField&&(e.SortField=t.meta.arg.sortField)}),e.addCase(this.SetStatusToChanged.pending,function(e){e.FetchStatus="changed"})},e}();t.default=p},32014:function(e,t,n){"use strict";var o,r=this&&this.__extends||(o=function(e,t){return o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},o(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}o(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),a=this&&this.__assign||function(){return a=Object.assign||function(e){for(var t,n=1,o=arguments.length;n0&&r[r.length-1])||6!==c[0]&&2!==c[0])){i=0;continue}if(3===c[0]&&(!r||c[1]>r[0]&&c[1]0||(e.AddStatus="error",e.Error={Message:null!==(n=t.error.message)&&void 0!==n?n:"",Action:"ADD",Time:(new Date).toString()})}),t.addCase(e.Update.pending,function(e,t){e.UpdateStatus="loading",e.ActiveID.push(t.meta.requestId)}),t.addCase(e.Update.fulfilled,function(e,t){e.UpdateStatus="changed",e.ActiveID=e.ActiveID.filter(function(e){return e!==t.meta.requestId})}),t.addCase(e.Update.rejected,function(e,t){var n;e.ActiveID=e.ActiveID.filter(function(e){return e!==t.meta.requestId}),e.ActiveID.length>0||(e.UpdateStatus="error",e.Error={Message:null!==(n=t.error.message)&&void 0!==n?n:"",Action:"UPDATE",Time:(new Date).toString()})}),t.addCase(e.Delete.pending,function(e,t){e.DeleteStatus="loading",e.ActiveID.push(t.meta.requestId)}),t.addCase(e.Delete.fulfilled,function(e,t){e.DeleteStatus="changed",e.ActiveID=e.ActiveID.filter(function(e){return e!==t.meta.requestId})}),t.addCase(e.Delete.rejected,function(e,t){var n;e.ActiveID=e.ActiveID.filter(function(e){return e!==t.meta.requestId}),e.ActiveID.length>0||(e.DeleteStatus="error",e.Error={Message:null!==(n=t.error.message)&&void 0!==n?n:"",Action:"DELETE",Time:(new Date).toString()})})}}),this.Reducer=this.Slice.reducer},t}(p(n(35037)).default);t.default=u},36333:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0}),t.useInitializeWithFetch=function(e,t,n,o){var r=n(e.FetchStatus),a=n(e.ParentID);return i.useEffect(function(){"uninitiated"!==r&&"changed"!==r&&a==o||t(e.Fetch({parentID:o}))},[r,t]),r};var i=a(n(8674))},78938:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n0?null===(t=e.AlternateColors)||void 0===t?void 0:t.selected:null===(n=e.AlternateColors)||void 0===n?void 0:n.normal}))},[e.AlternateColors,e.Data.length]),a.default.useEffect(function(){switch(e.Type){case"Meter":p(e.Data.filter(function(e,t){return t<10}).map(function(e){return a.default.createElement("tr",{key:e.ID},a.default.createElement("td",null,e.Name),a.default.createElement("td",null,e.AssetKey),a.default.createElement("td",null,e.Location),a.default.createElement("td",null,e.Make),a.default.createElement("td",null,e.Model))}));break;case"Asset":p(e.Data.filter(function(e,t){return t<10}).map(function(e){return a.default.createElement("tr",{key:e.ID},a.default.createElement("td",null,e.AssetKey),a.default.createElement("td",null,e.AssetName),a.default.createElement("td",null,e.AssetType),a.default.createElement("td",null,e.VoltageKV))}));break;case"AssetGroup":p(e.Data.filter(function(e,t){return t<10}).map(function(e){return a.default.createElement("tr",{key:e.ID},a.default.createElement("td",null,e.Name),a.default.createElement("td",null,e.Assets),a.default.createElement("td",null,e.Meters))}));break;case"Station":p(e.Data.filter(function(e,t){return t<10}).map(function(e){return a.default.createElement("tr",{key:e.ID},a.default.createElement("td",null,e.Name),a.default.createElement("td",null,e.LocationKey),a.default.createElement("td",null,e.Meters),a.default.createElement("td",null,e.Assets))}));break;default:p([])}},[e.Data,e.Type]),a.default.createElement(a.default.Fragment,null,a.default.createElement("button",{className:"btn btn-block btn-sm btn-"+(e.Data.length>0?"warning":"primary"),style:M,onClick:function(t){t.preventDefault(),e.OnClick()},onMouseEnter:function(){return r(!0)},onMouseLeave:function(){return r(!1)}},function(e){switch(e){case"Meter":return"Meter";case"Asset":return"Asset";case"AssetGroup":return"Asset Group";case"Station":return"Substation";default:return e}}(e.Type)," ",e.Data.length>0?"("+e.Data.length+")":""),a.default.createElement("div",{style:{width:window.innerWidth/3,display:n?"block":"none",position:"absolute",backgroundColor:"#f1f1f1",boxShadow:"0px 8px 16px 0px rgba(0,0,0,0.2)",zIndex:1,right:0},onMouseEnter:function(){return r(!0)},onMouseLeave:function(){return r(!1)}},a.default.createElement("table",{className:"table"},a.default.createElement("thead",null,s),a.default.createElement("tbody",null,c))))}},4414:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n1||e.NoteTypes.length>1||e.NoteApplications.length>1;return c.createElement("div",{className:"row",style:{marginRight:0,marginLeft:0}},c.createElement("div",{className:t?"col-6":"col-12"},c.createElement(p.TextArea,{Record:e.Record,Rows:4,Field:"Note",Setter:function(t){return e.Setter(t)},Valid:function(){return null!=e.Record.Note&&e.Record.Note.length>0},Label:""})),t?c.createElement("div",{className:"col-6"},e.NoteTypes.length>1?c.createElement(p.Select,{Record:e.Record,Field:"NoteTypeID",Label:"Note for: ",Options:e.NoteTypes.map(function(e){return{Value:e.ID.toString(),Label:e.Name}}),Setter:function(t){return e.Setter(o(o({},t),{NoteTypeID:parseInt(t.NoteTypeID.toString(),10)}))}}):null,e.NoteTags.length>1?c.createElement(p.Select,{Record:e.Record,Field:"NoteTagID",Label:"Type: ",Options:e.NoteTags.map(function(e){return{Value:e.ID.toString(),Label:e.Name}}),Setter:function(t){return e.Setter(o(o({},t),{NoteTagID:parseInt(t.NoteTagID.toString(),10)}))}}):null,e.ShowApplications&&e.NoteApplications.length>1?c.createElement(p.Select,{Record:e.Record,Field:"NoteApplicationID",Label:"Application: ",Options:e.NoteApplications.map(function(e){return{Value:e.ID.toString(),Label:e.Name}}),Setter:function(t){return e.Setter(o(o({},t),{NoteApplicationID:parseInt(t.NoteApplicationID.toString(),10)}))}}):null):null)}t.default=function(e){var t=(0,M.useDispatch)(),n=void 0===e.AllowEdit||e.AllowEdit,r=void 0===e.AllowRemove||e.AllowRemove,a=void 0===e.AllowAdd||e.AllowAdd,i=1===e.NoteApplications.length||void 0!==e.DefaultApplication,p=void 0!==e.DefaultApplication?e.DefaultApplication:e.NoteApplications[0],z=void 0===e.ShowCard||e.ShowCard,O=c.useState(!1),f=O[0],h=O[1],A=c.useState("none"),m=A[0],q=A[1],v=(0,M.useSelector)(e.NoteSlice.Data),_=(0,M.useSelector)(e.NoteSlice.Status),y=(0,M.useSelector)(void 0===e.NoteSlice.ParentID?function(){return e.ReferenceTableID}:e.NoteSlice.ParentID),g=(0,M.useSelector)(e.NoteSlice.SortField),W=(0,M.useSelector)(e.NoteSlice.Ascending),L=c.useState(T()),R=L[0],w=L[1],S=c.useState([]),x=S[0],k=S[1];function T(){var t={ID:-1,ReferenceTableID:-1,NoteTagID:-1,NoteTypeID:-1,NoteApplicationID:-1,Timestamp:"",UserAccount:"",Note:""};return void 0!==e.ReferenceTableID&&(t.ReferenceTableID=e.ReferenceTableID),null!=p&&(t.NoteApplicationID=p.ID),e.NoteTypes.length>0&&(t.NoteTypeID=e.NoteTypes[0].ID),e.NoteTags.length>0&&(t.NoteTagID=e.NoteTags[0].ID),t}function N(n){t(e.NoteSlice.DBAction({verb:"POST",record:o(o({},n),{UserAccount:void 0,Timestamp:b().format("MM/DD/YYYY HH:mm")})})),w(T())}return c.useEffect(function(){"uninitiated"!==_&&"changed"!==_&&y===e.ReferenceTableID||t(e.NoteSlice.Fetch(e.ReferenceTableID))},[e.ReferenceTableID,t,_]),c.useEffect(function(){R.NoteTypeID>0||0===e.NoteTypes.length||w(function(t){return o(o({},t),{NoteTypeID:e.NoteTypes[0].ID})})},[e.NoteTypes]),c.useEffect(function(){R.NoteApplicationID>0||0===e.NoteApplications.length||w(function(t){return o(o({},t),{NoteApplicationID:e.NoteApplications[0].ID})})},[e.NoteApplications]),c.useEffect(function(){R.NoteTagID>0||0===e.NoteTags.length||w(function(t){return o(o({},t),{NoteTagID:e.NoteTags[0].ID})})},[e.NoteTags]),c.useEffect(function(){void 0!==R.ReferenceTableID&&w(function(t){return o(o({},t),{ReferenceTableID:void 0!==e.ReferenceTableID?e.ReferenceTableID:-1})})},[e.ReferenceTableID]),c.useEffect(function(){k(v.filter(function(t){return void 0===e.Filter||e.Filter(t)}))},[e.Filter,v]),"error"===_?c.createElement("div",{style:{width:"100%",height:"100%"}},c.createElement("div",{style:{height:"40px",margin:"auto",marginTop:"calc(50% - 20 px)"}},c.createElement(u.ServerErrorIcon,{Show:!0,Size:40}))):c.createElement("div",{className:z?"card":"",style:{border:"0px",maxHeight:e.MaxHeight,width:"100%"}},c.createElement(u.LoadingScreen,{Show:"loading"===_}),c.createElement("div",{className:void 0===e.ShowCard||e.ShowCard?"card-header":""},c.createElement("div",{className:"row"},c.createElement("div",{className:"col"},c.createElement("h4",null,void 0!==e.Title?e.Title:"Notes:")))),c.createElement("div",{className:z?"card-body":"",style:{maxHeight:e.MaxHeight-100,overflowY:"auto",width:"100%"}},a&&!z?c.createElement(c.Fragment,null,c.createElement(d,{Record:R,Setter:function(e){return w(e)},NoteTags:e.NoteTags,NoteTypes:e.NoteTypes,NoteApplications:e.NoteApplications,ShowApplications:!i}),c.createElement("div",{className:"btn-group mr-2"},c.createElement("button",{className:"btn btn-primary"+(null===R.Note||0===R.Note.length?" disabled":""),onClick:function(){null!==R.Note&&R.Note.length>0&&N(R)},"data-tooltip":"Add",style:{cursor:null===R.Note||0===R.Note.length?"not-allowed":"pointer"},onMouseOver:function(){return q("add")},onMouseOut:function(){return q("none")}},"Add Note"),c.createElement(u.ToolTip,{Show:"add"===m&&(null===R.Note||0===R.Note.length),Position:"top",Target:"Add"},c.createElement("p",null,c.createElement(s.ReactIcons.CrossMark,null)," A note needs to be entered. "))),c.createElement("div",{className:"btn-group mr-2"},c.createElement("button",{className:"btn btn-default"+(null===R.Note||0===R.Note.length?" disabled":""),onClick:function(){return w(function(e){return o(o({},e),{Note:""})})},style:{cursor:null===R.Note||0===R.Note.length?"not-allowed":"pointer"},"data-tooltip":"Remove",onMouseOver:function(){return q("clear")},onMouseOut:function(){return q("none")}},"Clear"),c.createElement(u.ToolTip,{Show:"clear"===m&&(null===R.Note||0===R.Note.length),Position:"top",Target:"Remove"},c.createElement("p",null,c.createElement(s.ReactIcons.CrossMark,null)," The note field is already empty. ")))):null,c.createElement("div",null,c.createElement(l.Table,{TableClass:"table table-hover",Data:x,SortKey:g,Ascending:W,OnSort:function(n){void 0!==n.colField&&(n.colField===g?t(e.NoteSlice.Sort({SortField:g,Ascending:W})):t(e.NoteSlice.Sort({SortField:n.colField,Ascending:!0})))},OnClick:function(){},TbodyStyle:{maxHeight:e.MaxHeight-300},Selected:function(){return!1},KeySelector:function(e){return e.ID}},c.createElement(l.Column,{Key:"Note",Field:"Note",HeaderStyle:{width:"50%"},RowStyle:{width:"50%"}},"Note"),c.createElement(l.Column,{Key:"Timestamp",Field:"Timestamp",HeaderStyle:{width:"auto"},RowStyle:{width:"auto"},Content:function(e){return b.utc(e.item.Timestamp).format("MM/DD/YYYY HH:mm")}},"Time"),c.createElement(l.Column,{Key:"UserAccount",Field:"UserAccount",HeaderStyle:{width:"auto"},RowStyle:{width:"auto"}},"User"),e.children,e.NoteTags.length>1?c.createElement(l.Column,{Key:"NoteTagID",Field:"NoteTagID",HeaderStyle:{width:"auto"},RowStyle:{width:"auto"},Content:function(t){var n;return null===(n=e.NoteTags.find(function(e){return e.ID===t.item.NoteTagID}))||void 0===n?void 0:n.Name}},"Type"):c.createElement(c.Fragment,null),e.NoteApplications.length>1?c.createElement(l.Column,{Key:"NoteApplicationID",Field:"NoteApplicationID",HeaderStyle:{width:"auto"},RowStyle:{width:"auto"},Content:function(t){var n;return null===(n=e.NoteApplications.find(function(e){return e.ID===t.item.NoteApplicationID}))||void 0===n?void 0:n.Name}},"Application"):c.createElement(c.Fragment,null),c.createElement(l.Column,{Key:"buttons",HeaderStyle:{width:"auto"},RowStyle:{width:"auto"},Content:function(o){return c.createElement(c.Fragment,null,n?c.createElement("button",{className:"btn btn-sm",onClick:function(){return e=o.item,w(e),void h(!0);var e}},c.createElement(s.ReactIcons.Pencil,null)):null,r?c.createElement("button",{className:"btn btn-sm",onClick:function(){return t(e.NoteSlice.DBAction({verb:"DELETE",record:o.item}))}},c.createElement(s.ReactIcons.TrashCan,null)):null)}}," "))),a&&z?c.createElement(d,{Record:R,Setter:function(e){return w(e)},NoteTags:e.NoteTags,NoteTypes:e.NoteTypes,NoteApplications:e.NoteApplications,ShowApplications:!i}):null,c.createElement(u.Modal,{Show:f,Title:"Edit Note",ShowCancel:!0,CallBack:function(o){0===R.Note.length&&o||(h(!1),o&&n&&t(e.NoteSlice.DBAction({verb:"PATCH",record:R})),w(T()))},DisableConfirm:null==R.Note||0===R.Note.length,ShowX:!0,ConfirmShowToolTip:null==R.Note||0===R.Note.length,ConfirmToolTipContent:c.createElement("p",null,c.createElement(s.ReactIcons.CrossMark,null)," An empty Note can not be saved.")},c.createElement(d,{ShowApplications:!i,Record:R,Setter:function(e){return w(e)},NoteTags:e.NoteTags,NoteTypes:e.NoteTypes,NoteApplications:e.NoteApplications}))),a&&z?c.createElement("div",{className:"card-footer"},c.createElement("div",{className:"btn-group mr-2"},c.createElement("button",{className:"btn btn-primary"+(null===R.Note||0===R.Note.length?" disabled":""),onClick:function(){null!==R.Note&&R.Note.length>0&&N(R)},"data-tooltip":"Add",style:{cursor:null===R.Note||0===R.Note.length?"not-allowed":"pointer"},onMouseOver:function(){return q("add")},onMouseOut:function(){return q("none")}},"Add Note"),c.createElement(u.ToolTip,{Show:"add"===m&&(null===R.Note||0===R.Note.length),Position:"top",Target:"Add"},c.createElement("p",null,c.createElement(s.ReactIcons.CrossMark,null)," A note needs to be entered."))),c.createElement("div",{className:"btn-group mr-2"},c.createElement("button",{className:"btn btn-default"+(null===R.Note||0===R.Note.length?" disabled":""),onClick:function(){return w(function(e){return o(o({},e),{Note:""})})},style:{cursor:null===R.Note||0===R.Note.length?"not-allowed":"pointer"},"data-tooltip":"Remove",onMouseOver:function(){return q("clear")},onMouseOut:function(){return q("none")}},"Clear"),c.createElement(u.ToolTip,{Show:"clear"===m&&(null===R.Note||0===R.Note.length),Position:"top",Target:"Remove"},c.createElement("p",null,c.createElement(s.ReactIcons.CrossMark,null)," The note field is already empty. ")))):null,!a&&z?c.createElement("div",{className:void 0===e.ShowCard||e.ShowCard?"card-footer":""}," "):null)}},47010:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0}),t.useCSVFieldEditContext=t.CSVFieldEditContext=void 0;var i=a(n(8674));t.CSVFieldEditContext=i.createContext({Value:"",SetValue:function(e){},Validate:function(e){return!0},AllRecordValues:{},Feedback:"",SelectOptions:[]}),t.useCSVFieldEditContext=function(){return i.useContext(t.CSVFieldEditContext)}},67395:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t},i=this&&this.__awaiter||function(e,t,n,o){return new(n||(n=Promise))(function(r,a){function i(e){try{p(o.next(e))}catch(e){a(e)}}function c(e){try{p(o.throw(e))}catch(e){a(e)}}function p(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(i,c)}p((o=o.apply(e,t||[])).next())})},c=this&&this.__generator||function(e,t){var n,o,r,a,i={label:0,sent:function(){if(1&r[0])throw r[1];return r[1]},trys:[],ops:[]};return a={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function c(c){return function(p){return function(c){if(n)throw new TypeError("Generator is already executing.");for(;a&&(a=0,c[0]&&(i=0)),i;)try{if(n=1,o&&(r=2&c[0]?o.return:c[0]?o.throw||((r=o.return)&&r.call(o),0):o.next)&&!(r=r.call(o,c[1])).done)return r;switch(o=0,r&&(c=[2&c[0],r.value]),c[0]){case 0:case 1:r=c;break;case 4:return i.label++,{value:c[1],done:!1};case 5:i.label++,o=c[1],c=[0];continue;case 7:c=i.ops.pop(),i.trys.pop();continue;default:if(!((r=(r=i.trys).length>0&&r[r.length-1])||6!==c[0]&&2!==c[0])){i=0;continue}if(3===c[0]&&(!r||c[1]>r[0]&&c[1]1&&n.push("All rows must contain the same value for ".concat(r.Label,"."))),[2]}})},a=0,i=e.AdditionalProps.Fields,u.label=1;case 1:return a=b)){for(var M=p([],e.AdditionalProps.Headers,!0),d=null===(a=e.AdditionalProps)||void 0===a?void 0:a.Headers.length;d=u)){for(var b=p([],e.AdditionalProps.Headers,!0),M=null===(i=e.AdditionalProps)||void 0===i?void 0:i.Headers.length;M0?p(p([],e,!0),Array(t).fill(""),!0):e});null===(l=e.AdditionalProps)||void 0===l||l.SetHeaders(b),null===(s=e.AdditionalProps)||void 0===s||s.SetData(d)}}},[null===(v=e.AdditionalProps)||void 0===v?void 0:v.Fields,null===(_=e.AdditionalProps)||void 0===_?void 0:_.Headers,null===(y=e.AdditionalProps)||void 0===y?void 0:y.Data]),s.useEffect(function(){var t;if(null!=e.AdditionalProps&&0===e.Errors.length){var n=[];null===(t=e.AdditionalProps)||void 0===t||t.Data.forEach(function(t){var o,r={};null===(o=e.AdditionalProps)||void 0===o||o.Headers.forEach(function(n,o){var a,i,c=null===(a=e.AdditionalProps)||void 0===a?void 0:a.HeaderMap.get(n);if(null!=c){var p=null===(i=e.AdditionalProps)||void 0===i?void 0:i.Fields.find(function(e){return e.Field===c});if(null!=p){var l=t[o+1];r=p.Process(l,r,p.Field)}}}),n.push(r)}),e.SetData(n)}},[null===(g=e.AdditionalProps)||void 0===g?void 0:g.Data,null===(W=e.AdditionalProps)||void 0===W?void 0:W.Headers,null===(L=e.AdditionalProps)||void 0===L?void 0:L.HeaderMap,null===(R=e.AdditionalProps)||void 0===R?void 0:R.Fields,e.Errors]);var te=s.useCallback(function(t){var n,o,r;if(null!=e.AdditionalProps&&0!==(null===(n=e.AdditionalProps)||void 0===n?void 0:n.Fields.length)){var a=null===(o=e.AdditionalProps)||void 0===o?void 0:o.HeaderMap.get(t),i=Array.from(null===(r=e.AdditionalProps)||void 0===r?void 0:r.HeaderMap.entries()).filter(function(e){var n=e[0],o=e[1];return n!==t&&null!=o}).map(function(e){return e[1]}),c=e.AdditionalProps.Fields.filter(function(e){return!i.includes(e.Field)||e.Field===a}).map(function(e){return{Value:e.Field,Label:e.Label}}),p=e.AdditionalProps.Fields.find(function(e){return e.Field===a}),l=null!=(null==p?void 0:p.Help)?null==p?void 0:p.Help:void 0;return s.createElement(M.Select,{Record:{Header:t,Value:a},EmptyOption:!0,Label:" ",Help:l,Options:c,Field:"Value",Setter:function(t){return function(t,n){var o,r;return null===(o=e.AdditionalProps)||void 0===o?void 0:o.SetHeaderMap(new Map(null===(r=e.AdditionalProps)||void 0===r?void 0:r.HeaderMap).set(t,n))}(t.Header,t.Value)}})}},[null===(w=e.AdditionalProps)||void 0===w?void 0:w.Fields,null===(S=e.AdditionalProps)||void 0===S?void 0:S.HeaderMap]),ne=s.useCallback(function(t,n,o){var r,a,i,c=p([],null!==(a=null===(r=e.AdditionalProps)||void 0===r?void 0:r.Data)&&void 0!==a?a:[],!0);c[t][n]=o,null===(i=e.AdditionalProps)||void 0===i||i.SetData(c)},[null===(x=e.AdditionalProps)||void 0===x?void 0:x.Data]);return s.createElement(s.Fragment,null,s.createElement("div",{className:"container-fluid d-flex flex-column p-0 h-100"},s.createElement(f.default,{ClassName:"row h-100",ErrorMessage:"Error loading page."},s.createElement("div",{className:"row h-100"},s.createElement("div",{className:"col-12 d-flex flex-column h-100"},0!==P.length?s.createElement(s.Fragment,null,$>0&&Z>0?s.createElement("div",{className:"row"},s.createElement("div",{className:"col-12"},s.createElement(b.Alert,{Class:"alert-info",ReTrigger:$+Z},s.createElement("p",{style:{whiteSpace:"nowrap"}},"Missing data cells were added to meet the number of required fields."),s.createElement("hr",null),s.createElement("p",{style:{whiteSpace:"nowrap"}},"Missing headers were added to meet the number of required fields.")))):$>0||Z>0?s.createElement("div",{className:"row"},s.createElement("div",{className:"col-12"},s.createElement(b.Alert,{Class:"alert-info",ReTrigger:$>0?$:Z},s.createElement("p",{style:{whiteSpace:"nowrap"}},$>0?"Missing data cells were added to meet the number of required fields.":"Missing headers were added to meet the number of required fields.")))):null,s.createElement("div",{className:"row flex-grow-1",style:{overflowY:"hidden"}},s.createElement("div",{className:"col-12 h-100"},s.createElement(d.Table,{Data:P,key:null===(k=e.AdditionalProps)||void 0===k?void 0:k.Headers.join(","),SortKey:"",Ascending:!1,OnSort:function(){},KeySelector:function(e){return e[0]},TableClass:"table",TableStyle:{height:"100%",width:150*(null!==(N=null===(T=e.AdditionalProps)||void 0===T?void 0:T.Headers.length)&&void 0!==N?N:0)}},null===(E=e.AdditionalProps)||void 0===E?void 0:E.Headers.map(function(t,n){return s.createElement(d.Column,{Key:t,Field:n+1,AllowSort:!1,Content:function(n){var o,r,a=n.item,i=n.field;if(null!=e.AdditionalProps){var c=null===(o=e.AdditionalProps)||void 0===o?void 0:o.HeaderMap.get(t),p=e.AdditionalProps.Fields.find(function(e){return e.Field===c});if(null==p)return a[i];var l=a[i],u={};return null===(r=e.AdditionalProps)||void 0===r||r.Headers.forEach(function(t,n){var o,r=null===(o=e.AdditionalProps)||void 0===o?void 0:o.HeaderMap.get(t);null!=r&&(u[r]=a[n+1])}),s.createElement(h.CSVFieldEditContext.Provider,{value:{Value:l,SetValue:function(e){return ne(parseInt(a[0]),i,e)},Validate:p.Validate,Feedback:p.Feedback,AllRecordValues:u,SelectOptions:p.SelectOptions}},p.EditComponent)}}},function(t){var n;if(null!=e.AdditionalProps){var o=null===(n=e.AdditionalProps)||void 0===n?void 0:n.HeaderMap.get(t);return null==o||null==e.AdditionalProps.Fields.find(function(e){return e.Field===o})?t:null}}(t),te(t))}),s.createElement(d.Column,{Key:"delete",Field:0,AllowSort:!1,RowStyle:{textAlign:"right"},Content:function(t){var n=t.item;return s.createElement("button",{className:"btn",onClick:function(){return function(t){var n,o,r,a=p([],null!==(o=null===(n=e.AdditionalProps)||void 0===n?void 0:n.Data)&&void 0!==o?o:[],!0);a.splice(t,1),null===(r=e.AdditionalProps)||void 0===r||r.SetData(a)}(parseInt(n[0]))}},s.createElement(z.ReactIcons.TrashCan,{Color:"red"}))}},"")))),s.createElement("div",{className:"row"},s.createElement("div",{className:"col-12"},s.createElement(d.Paging,{Current:Y+1,Total:H,SetPage:function(e){return X(e-1)}})))):null)))))}var q=function(e,t,n){var o=(0,u.CsvStringToArray)(e),r=t?o[0]:[],a=t?o.slice(1):o;a=a.map(function(e,t){return p([t.toString()],e,!0)});var i=!1,c=!1;if(t){if(r.length-1},KeySelector:function(e){return e.ID}},e.children)),"multiple"===e.Type?p.createElement("div",{className:"col",style:{width:"40%"}},p.createElement("div",{style:{width:"100%"}},p.createElement("h3",null," Current Selection ")),p.createElement(c.Table,{TableClass:"table table-hover",Data:M,SortKey:O,Ascending:A,OnSort:function(e){if(e.colKey===O){var t=u.orderBy(M,[e.colKey],[A?"desc":"asc"]);m(!A),d(t)}else t=u.orderBy(M,[e.colKey],["asc"]),m(!A),d(t),f(e.colKey)},OnClick:function(e){return d(i([],M.filter(function(t){return t.ID!==e.row.ID}),!0))},Selected:function(){return!1},KeySelector:function(e){return e.ID}},e.children)):null)))};var c=n(23514),p=a(n(8674)),l=n(90547),s=n(68976),u=n(42005),b=n(36400)},22444:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.AvailableQuickSelects=void 0,t.getFormat=u;var c=a(n(8674)),p=n(24611),l=i(n(328)),s=i(n(36302));function u(e){return"date"==e?"YYYY-MM-DD":"time"==e?"HH:mm:ss.SSS":"MM/DD/YYYY HH:mm:ss.SSS"}t.default=function(e){var n,o=c.useMemo(function(){return t.AvailableQuickSelects.filter(function(t){return!t.hideQuickPick(e.DateUnit)})},[e.DateUnit]);return c.createElement(M,{AddRow:null===(n=e.AddRowContainer)||void 0===n||n},o.map(function(t,n){var r;return n%3!=0?null:c.createElement("div",{key:n,className:b(e.DateTimeSetting,null!==(r=e.SplitSelects)&&void 0!==r&&r),style:{paddingLeft:"startEnd"===e.DateTimeSetting?0:n%9==0?15:0,paddingRight:"startEnd"===e.DateTimeSetting?2:n%18==6||n%18==15?15:2,marginTop:10}},c.createElement("ul",{className:"list-group",key:n},c.createElement("li",{key:n,style:{cursor:"pointer"},onClick:function(){var t=(0,p.getTimeWindowFromFilter)(o[n].createFilter(e.Timezone,e.DateUnit),e.Format);e.SetFilter(t.start,t.end,t.unit,t.duration),e.SetActiveQP(n)},className:"item badge badge-"+(n==e.ActiveQP?"primary":"secondary")},o[n].label),n+1t?"col-2":!e.ShowQuickSelects&&e.ContainerWidth>375?"col-6":"col-12"},[e.ShowQuickSelects,e.ContainerWidth]),r=c.useMemo(function(){return e.ContainerWidth>t?"col-8":"col-12"},[e.ContainerWidth]);return e.ContainerWidth>t?c.createElement("div",{className:"row m-0"},c.createElement("div",{className:o},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"start",Help:e.HelpMessage,Setter:n,Label:"Start",Type:e.DateUnit,Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),c.createElement("div",{className:o},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"end",Help:e.HelpMessage,Setter:n,Label:"End",Type:e.DateUnit,Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),e.ShowQuickSelects?c.createElement("div",{className:r},c.createElement(s.default,{DateTimeSetting:"startEnd",Timezone:e.Timezone,ActiveQP:e.ActiveQP,SetActiveQP:e.SetActiveQP,SetFilter:e.SetFilter,Format:e.Format,DateUnit:e.DateUnit})):null):c.createElement(c.Fragment,null,c.createElement("div",{className:"row m-0"},c.createElement("div",{className:o},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"start",Help:e.HelpMessage,Setter:n,Label:"Start",Type:e.DateUnit,Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),c.createElement("div",{className:o},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"end",Help:e.HelpMessage,Setter:n,Label:"End",Type:e.DateUnit,Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy}))),e.ShowQuickSelects?c.createElement("div",{className:r},c.createElement(s.default,{DateTimeSetting:"startEnd",Timezone:e.Timezone,ActiveQP:e.ActiveQP,SetActiveQP:e.SetActiveQP,SetFilter:e.SetFilter,Format:e.Format,DateUnit:e.DateUnit,SplitSelects:e.ContainerWidth<541})):null)}},22758:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var c=a(n(8674)),p=n(90782),l=n(24611),s=i(n(22444)),u=n(68976);t.default=function(e){var t,n,o,r,a,i,b=1768,M=c.useState(!1),d=M[0],z=M[1],O=c.useCallback(function(t){var n=(0,l.getTimeWindowFromFilter)({start:t.start,end:t.end},e.Format);e.SetTimeWindowFilter(n),e.SetActiveQP(-1)},[e.Format]),f=c.useMemo(function(){return e.ShowQuickSelects&&e.ContainerWidth>b?"col-2":e.ContainerWidth>612?"col-6":"col-12"},[e.ShowQuickSelects,e.DateUnit,e.ContainerWidth]),h=c.useMemo(function(){return e.ContainerWidth>b?"col-8":"col-12"},[e.DateUnit,e.ContainerWidth]);return e.ContainerWidth>b?c.createElement("div",{className:"row m-0"},c.createElement("div",{className:f},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"start",Help:e.HelpMessage,Setter:O,Label:"Start",Type:null!==(t=e.DateUnit)&&void 0!==t?t:"datetime-local",Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),c.createElement("div",{className:f},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"end",Help:e.HelpMessage,Setter:O,Label:"End",Type:null!==(n=e.DateUnit)&&void 0!==n?n:"datetime-local",Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),e.ShowQuickSelects?c.createElement("div",{className:h},c.createElement(s.default,{DateTimeSetting:"startEnd",Timezone:e.Timezone,ActiveQP:e.ActiveQP,SetActiveQP:e.SetActiveQP,SetFilter:e.SetFilter,Format:e.Format,DateUnit:e.DateUnit})):null):e.ContainerWidth>612?c.createElement("div",{className:"row m-0"},c.createElement("div",{className:f},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"start",Help:e.HelpMessage,Setter:O,Label:"Start",Type:null!==(o=e.DateUnit)&&void 0!==o?o:"datetime-local",Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),c.createElement("div",{className:f},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"end",Help:e.HelpMessage,Setter:O,Label:"End",Type:null!==(r=e.DateUnit)&&void 0!==r?r:"datetime-local",Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),c.createElement("div",{className:"row m-0 w-100"},e.ShowQuickSelects?c.createElement("div",{className:h},c.createElement(s.default,{DateTimeSetting:"startEnd",Timezone:e.Timezone,ActiveQP:e.ActiveQP,SetActiveQP:e.SetActiveQP,SetFilter:e.SetFilter,Format:e.Format,DateUnit:e.DateUnit})):null)):c.createElement("div",{className:"row m-0"},c.createElement("div",{className:f},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"start",Help:e.HelpMessage,Setter:O,Label:"Start",Type:null!==(a=e.DateUnit)&&void 0!==a?a:"datetime-local",Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),c.createElement("div",{className:f},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"end",Help:e.HelpMessage,Setter:O,Label:"End",Type:null!==(i=e.DateUnit)&&void 0!==i?i:"datetime-local",Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),e.ShowQuickSelects?c.createElement("div",{className:"row m-0 w-100"},c.createElement("div",{className:"col-12 d-flex align-items-center justify-content-center"},c.createElement("button",{className:"btn btn-primary w-100",onClick:function(){return z(!0)}},"Quick Selects")),c.createElement(u.Modal,{Show:d,Title:"Quick Selects",CallBack:function(){return z(!1)},ShowX:!0,ShowCancel:!1,ShowConfirm:!1,Size:"xlg"},c.createElement("div",{className:h},c.createElement(s.default,{DateTimeSetting:"startEnd",Timezone:e.Timezone,ActiveQP:e.ActiveQP,SetActiveQP:e.SetActiveQP,SetFilter:e.SetFilter,Format:e.Format,DateUnit:e.DateUnit})))):null)}},38401:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n634?"col-5":"col-6":e.ContainerWidth>454?"col-6":"col-12"},[e.ShowQuickSelects,e.DateUnit,e.ContainerWidth]);return e.ContainerWidth>634?c.createElement("div",{className:"row m-0"},c.createElement("div",{className:n},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"start",Help:e.HelpMessage,Setter:t,Label:"Start",Type:e.DateUnit,Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),c.createElement("div",{className:n},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"end",Help:e.HelpMessage,Setter:t,Label:"End",Type:e.DateUnit,Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),e.ShowQuickSelects?c.createElement(s.default,{DateTimeSetting:"startEnd",Timezone:e.Timezone,ActiveQP:e.ActiveQP,SetActiveQP:e.SetActiveQP,SetFilter:e.SetFilter,Format:e.Format,DateUnit:e.DateUnit,AddRowContainer:!1}):null):c.createElement(c.Fragment,null,c.createElement("div",{className:"row m-0"},c.createElement("div",{className:n},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"start",Help:e.HelpMessage,Setter:t,Label:"Start",Type:e.DateUnit,Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),c.createElement("div",{className:n},c.createElement(p.DatePicker,{Record:e.TimeWindowFilter,Field:"end",Help:e.HelpMessage,Setter:t,Label:"End",Type:e.DateUnit,Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy}))),e.ShowQuickSelects?c.createElement(s.default,{DateTimeSetting:"startEnd",Timezone:e.Timezone,ActiveQP:e.ActiveQP,SetActiveQP:e.SetActiveQP,SetFilter:e.SetFilter,Format:e.Format,DateUnit:e.DateUnit}):null)}},24611:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.getTimeWindowFromFilter=O;var c=a(n(8674)),p=n(31688),l=i(n(328)),s=n(22444),u=i(n(42005)),b=i(n(38401)),M=n(76752),d=i(n(53923)),z=n(36400);function O(e,t){var n,o,r,a,i=function(e,n,o,r){return{start:e.format(t),end:n.format(t),unit:o,duration:r}};if("start"in e&&"duration"in e)return n=(0,l.default)(e.start,t),a=e.duration,r=e.unit,i(n,o=(0,p.addDuration)(n,a,r),r,a);if("end"in e&&"duration"in e)return o=(0,l.default)(e.end,t),a=e.duration,r=e.unit,i(n=(0,p.addDuration)(o,-a,r),o,r,a);if("start"in e&&"end"in e)return n=(0,l.default)(e.start,t),o=(0,l.default)(e.end,t),r=(0,p.findAppropriateUnit)(n,o),a=o.diff(n,r),i(n,o,r,a);throw TypeError("Unexpected type in getTimeWindowFromFilter, filter is: ".concat(e.toString()))}t.default=function(e){var t,n,o,r,a,i=c.useRef(null),p=(0,M.useGetContainerPosition)(i).width,l=(0,s.getFormat)(e.format),f=c.useState(-1),h=f[0],A=f[1],m=c.useState(O(e.filter,l)),q=m[0],v=m[1],_=c.useState(!1),y=_[0],g=_[1];c.useEffect(function(){W(q,e.filter)||e.setFilter(q.start,q.end,q.unit,q.duration)},[q]),c.useEffect(function(){if(!W(q,e.filter)){var t=O(e.filter,l);v(t)}},[e.filter]),c.useEffect(function(){null!=e.setCollapsed&&e.setCollapsed(y)},[y]);var W=function(e,t){var n=O(t,l);return u.default.isEqual(e,n)},L=null===(t=e.showHelpMessage)||void 0===t||t?"All times shown are in system time (".concat(e.timeZone,")."):void 0;return c.createElement("fieldset",{className:"border",style:{padding:"10px",height:"100%",overflow:"hidden"},ref:i},c.createElement("legend",{className:"w-auto",style:{fontSize:"large"}},c.createElement("div",{className:"d-flex align-items-center"},c.createElement("span",{className:"mr-2"},"Date/Time Filter:"),null!==(n=e.enableCollapse)&&void 0!==n&&n?c.createElement("button",{type:"button",className:"btn p-0 ml-auto",onClick:function(){return g(function(e){return!e})}},y?c.createElement(z.ReactIcons.ArrowDropDown,null):c.createElement(z.ReactIcons.ArrowDropUp,null)):null)),y?null:"startEnd"===e.dateTimeSetting?c.createElement(b.default,{TimeWindowFilter:q,SetTimeWindowFilter:v,Timezone:e.timeZone,ActiveQP:h,SetActiveQP:A,SetFilter:e.setFilter,Accuracy:e.accuracy,Format:l,DateUnit:null!==(o=e.format)&&void 0!==o?o:"datetime-local",ShowQuickSelects:e.showQuickSelect,ContainerWidth:p,HelpMessage:L}):"startWindow"===e.dateTimeSetting?c.createElement(d.default,{TimeWindowFilter:q,SetTimeWindowFilter:v,Timezone:e.timeZone,ActiveQP:h,SetActiveQP:A,SetFilter:e.setFilter,Accuracy:e.accuracy,Format:l,DateUnit:null!==(r=e.format)&&void 0!==r?r:"datetime-local",ShowQuickSelects:e.showQuickSelect,ContainerWidth:p,HelpMessage:L,Window:"start"}):c.createElement(d.default,{TimeWindowFilter:q,SetTimeWindowFilter:v,Timezone:e.timeZone,ActiveQP:h,SetActiveQP:A,SetFilter:e.setFilter,Accuracy:e.accuracy,Format:l,DateUnit:null!==(a=e.format)&&void 0!==a?a:"datetime-local",ShowQuickSelects:e.showQuickSelect,ContainerWidth:p,HelpMessage:L,Window:"end"}))}},31688:function(e,t,n){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.dateTimeFormat=t.units=void 0,t.findAppropriateUnit=function(e,n){for(var o=n.diff(e,t.units[7],!0),r=7;r>=1;r--){if(Number.isInteger(o))return t.units[r];var a=r-1;if((o=n.diff(e,t.units[a],!0))>65e3)return o=n.diff(e,t.units[r],!0),t.units[r]}return t.units[0]},t.getStartEndTime=function(e,t,n){return[a(e,-t,n),a(e,t,n)]},t.addDuration=a,t.getMoment=function(e,t,n){return void 0===n?(0,r.default)(e,null!=t?t:"MM/DD/YYYY HH:mm:ss.SSS"):(0,r.default)(e+" "+n,"MM/DD/YYYY HH:mm:ss.SSS")},t.readableUnit=function(e){return"y"==e?"Year(s)":"M"==e?"Month(s)":"w"==e?"Week(s)":"d"==e?"Day(s)":"h"==e?"Hour(s)":"m"==e?"Minute(s)":"s"==e?"Second(s)":"Millisecond(s)"};var r=o(n(328));function a(e,n,o){var r=e.clone(),a=n>0?Math.floor(n):Math.ceil(n);if(a==(n>0?Math.ceil(n):Math.floor(n))&&t.units.findIndex(function(e){return e==o})>=4)return r.add(n,o);r.add(a,o);var i=r.clone().add(Math.sign(n),o).diff(r,"h",!0)*Math.abs(n-a);return r.add(i,"h")}t.units=["ms","s","m","h","d","w","M","y"],t.dateTimeFormat="DD MM YYYY hh:mm:ss.SSS"},53923:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var c=n(90782),p=a(n(8674)),l=n(24611),s=i(n(22444)),u=i(n(11411));t.default=function(e){var t,n,o,r,a=e.Window,i=e.Window.charAt(0).toUpperCase()+e.Window.slice(1),b="start"===e.Window?"startWindow":"endWindow",M=p.useCallback(function(t){if("start"===e.Window){var n=(0,l.getTimeWindowFromFilter)({start:t.start,duration:t.duration,unit:t.unit},e.Format);e.SetTimeWindowFilter(n),e.SetActiveQP(-1)}else n=(0,l.getTimeWindowFromFilter)({end:t.end,duration:t.duration,unit:t.unit},e.Format),e.SetTimeWindowFilter(n),e.SetActiveQP(-1)},[e.Window,e.SetTimeWindowFilter,e.SetActiveQP,e.Format]);return e.ShowQuickSelects?e.ContainerWidth>898?p.createElement("div",{className:"row m-0"},p.createElement("div",{className:"col-4"},p.createElement(c.DatePicker,{Record:e.TimeWindowFilter,Field:a,Help:e.HelpMessage,Setter:M,Label:i,Type:null!==(n=e.DateUnit)&&void 0!==n?n:"datetime-local",Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy}),p.createElement(u.default,{Filter:e.TimeWindowFilter,SetFilter:e.SetTimeWindowFilter,SetActiveQP:e.SetActiveQP,Format:e.Format,ShowQuickSelect:e.ShowQuickSelects,Window:e.Window})),p.createElement("div",{className:"col-8 pt-3"},p.createElement(s.default,{DateTimeSetting:b,Timezone:e.Timezone,ActiveQP:e.ActiveQP,SetActiveQP:e.SetActiveQP,SetFilter:e.SetFilter,Format:e.Format,DateUnit:e.DateUnit}))):e.ContainerWidth>611?p.createElement(p.Fragment,null,p.createElement("div",{className:"row m-0"},p.createElement("div",{className:"col-6"},p.createElement(c.DatePicker,{Record:e.TimeWindowFilter,Field:a,Help:e.HelpMessage,Setter:M,Label:i,Type:null!==(o=e.DateUnit)&&void 0!==o?o:"datetime-local",Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy})),p.createElement("div",{className:"col-6"},p.createElement(u.default,{Filter:e.TimeWindowFilter,SetFilter:e.SetTimeWindowFilter,SetActiveQP:e.SetActiveQP,Format:e.Format,ShowQuickSelect:e.ShowQuickSelects,Window:e.Window}))),p.createElement("div",{className:"row m-0"},p.createElement("div",{className:"col-12"},p.createElement(s.default,{DateTimeSetting:b,Timezone:e.Timezone,ActiveQP:e.ActiveQP,SetActiveQP:e.SetActiveQP,SetFilter:e.SetFilter,Format:e.Format,DateUnit:e.DateUnit})))):p.createElement(p.Fragment,null,p.createElement("div",{className:"row m-0"},p.createElement("div",{className:"col-12"},p.createElement(c.DatePicker,{Record:e.TimeWindowFilter,Field:a,Help:e.HelpMessage,Setter:M,Label:i,Type:null!==(r=e.DateUnit)&&void 0!==r?r:"datetime-local",Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy}))),p.createElement("div",{className:"row m-0"},p.createElement("div",{className:"col-12"},p.createElement(u.default,{Filter:e.TimeWindowFilter,SetFilter:e.SetTimeWindowFilter,SetActiveQP:e.SetActiveQP,Format:e.Format,ShowQuickSelect:e.ShowQuickSelects,Window:e.Window}))),p.createElement("div",{className:"row m-0"},p.createElement("div",{className:"col-12"},p.createElement(s.default,{DateTimeSetting:b,Timezone:e.Timezone,ActiveQP:e.ActiveQP,SetActiveQP:e.SetActiveQP,SetFilter:e.SetFilter,Format:e.Format,DateUnit:e.DateUnit})))):p.createElement("div",{className:"row m-0"},p.createElement("div",{className:"col-12"},p.createElement(c.DatePicker,{Record:e.TimeWindowFilter,Field:a,Help:e.HelpMessage,Setter:M,Label:i,Type:null!==(t=e.DateUnit)&&void 0!==t?t:"datetime-local",Valid:function(){return!0},Format:e.Format,Accuracy:e.Accuracy}),p.createElement(u.default,{Filter:e.TimeWindowFilter,SetFilter:e.SetTimeWindowFilter,SetActiveQP:e.SetActiveQP,Format:e.Format,ShowQuickSelect:e.ShowQuickSelects,Window:e.Window})))}},11411:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var i=n(90782),c=a(n(8674)),p=n(24611),l=n(31688);t.default=function(e){var t=c.useCallback(function(t){"start"===e.Window?(e.SetFilter((0,p.getTimeWindowFromFilter)({start:t.start,duration:t.duration,unit:t.unit},e.Format)),e.SetActiveQP(-1)):(e.SetFilter((0,p.getTimeWindowFromFilter)({end:t.end,duration:t.duration,unit:t.unit},e.Format)),e.SetActiveQP(-1))},[e.Window,e.Format,e.SetActiveQP,e.SetActiveQP]);return c.createElement("div",{className:"form-group"},c.createElement("label",{style:{width:"100%",position:"relative",float:"left"}},"Span(","start"===e.Window?"+":"-",")"),c.createElement("div",{className:"row"},c.createElement("div",{className:"col-6"},c.createElement(i.Input,{Record:e.Filter,Field:"duration",Label:"",Valid:function(){return!0},Type:"number",Setter:t})),c.createElement("div",{className:"col-6"},c.createElement(i.Select,{Record:e.Filter,Label:"",Field:"unit",Options:l.units.map(function(e){return{Value:e,Label:(0,l.readableUnit)(e)}}),Setter:t}))))}},3200:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.ReadWriteControllerFunctions_Gemstone=t.ReadOnlyControllerFunctions_Gemstone=t.useInitializeWithFetch_Gemstone=t.ReadWriteGenericSlice_Gemstone=t.ReadOnlyGenericSlice_Gemstone=t.Gemstone=t.RoleAccessErrorPage=t.BulkUpload=t.CSVFieldEditContext=t.useCSVFieldEditContext=t.Pipelines=t.ErrorBoundary=t.DefaultSelects=t.SelectPopup=t.DefaultSearch=t.Note=t.NavBarFilterButton=t.EventCharacteristicFilter=t.EventTypeFilter=t.TimeWindowUtils=t.TimeFilter=void 0;var c=i(n(4414));t.Note=c.default;var p=n(14769);Object.defineProperty(t,"DefaultSearch",{enumerable:!0,get:function(){return p.DefaultSearch}});var l=i(n(29693));t.SelectPopup=l.default;var s=n(318);Object.defineProperty(t,"DefaultSelects",{enumerable:!0,get:function(){return s.DefaultSelects}});var u=i(n(49808));t.ErrorBoundary=u.default;var b=i(n(24611));t.TimeFilter=b.default;var M=i(n(27034));t.EventTypeFilter=M.default;var d=i(n(15903));t.EventCharacteristicFilter=d.default;var z=i(n(78938));t.NavBarFilterButton=z.default;var O=n(67395),f=a(n(31688));t.TimeWindowUtils=f;var h=i(n(93195));t.BulkUpload=h.default;var A=i(n(69508));t.RoleAccessErrorPage=A.default;var m=n(47010);Object.defineProperty(t,"useCSVFieldEditContext",{enumerable:!0,get:function(){return m.useCSVFieldEditContext}}),Object.defineProperty(t,"CSVFieldEditContext",{enumerable:!0,get:function(){return m.CSVFieldEditContext}});var q=n(29281);Object.defineProperty(t,"Gemstone",{enumerable:!0,get:function(){return q.Gemstone}});var v=i(n(35037));t.ReadOnlyGenericSlice_Gemstone=v.default;var _=i(n(32014));t.ReadWriteGenericSlice_Gemstone=_.default;var y=n(36333);Object.defineProperty(t,"useInitializeWithFetch_Gemstone",{enumerable:!0,get:function(){return y.useInitializeWithFetch}});var g=n(88172);Object.defineProperty(t,"ReadOnlyControllerFunctions_Gemstone",{enumerable:!0,get:function(){return g.ReadOnlyControllerFunctions}}),Object.defineProperty(t,"ReadWriteControllerFunctions_Gemstone",{enumerable:!0,get:function(){return g.ReadWriteControllerFunctions}});var W={CSV:O.useCSVPipeline};t.Pipelines=W},38411:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SVGIcons=void 0;var o,r=n(8674),a=n(38411);!function(e){e.DataContainer=r.createElement(a.ReactIcons.DataContainer,null),e.Cube=r.createElement(a.ReactIcons.Cube,null),e.House=r.createElement(a.ReactIcons.House,null),e.Document=r.createElement(a.ReactIcons.Document,null),e.ArrowForward=r.createElement(a.ReactIcons.ArrowForward,null),e.ArrowBackward=r.createElement(a.ReactIcons.ArrowBackward,null),e.ArrowDropUp=r.createElement(a.ReactIcons.ArrowDropUp,null),e.ArrowDropDown=r.createElement(a.ReactIcons.ArrowDropDown,null),e.Settings=r.createElement(a.ReactIcons.Settings,null),e.Filter=r.createElement(a.ReactIcons.Filter,null),e.Folder=r.createElement(a.ReactIcons.Folder,null),e.AlertPerson=r.createElement(a.ReactIcons.AlertPerson,null),e.AlertPeople=r.createElement(a.ReactIcons.AlertPeople,null),e.Alert=r.createElement(a.ReactIcons.Alert,null),e.TrashCan=r.createElement(a.ReactIcons.TrashCan,null),e.CrossMark=r.createElement(a.ReactIcons.CrossMark,null),e.CircledX=r.createElement(a.ReactIcons.CircledX,null),e.CircleCheck=r.createElement(a.ReactIcons.CircleCheckMark,null),e.Phone=r.createElement(a.ReactIcons.Phone,null),e.PhoneSettings=r.createElement(a.ReactIcons.PhoneSettings,null),e.AlertAdd=r.createElement(a.ReactIcons.AlertAdd,null),e.ReportAdd=r.createElement(a.ReactIcons.ReportAdd,null)}(o||(t.SVGIcons=o={}))},74086:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ReactIcons=t.SVGIcons=t.Point=t.Pan=t.Questionmark=t.RewindButton=t.FastForwardButton=t.PauseButton=t.StopButton=t.PlayButton=t.Scroll=t.House=t.MagnifyingGlass=t.FourWayArrow=t.InputNumbers=t.DNA=t.DownArrow=t.RightArrow=t.LeftArrow=t.UpArrow=t.Flag=t.Wrench=t.Spinner=t.Warning=t.Plus=t.CrossMark=t.TrashCan=t.Pencil=t.HeavyCheckMark=void 0;var o=n(90104);Object.defineProperty(t,"SVGIcons",{enumerable:!0,get:function(){return o.SVGIcons}});var r=n(38411);Object.defineProperty(t,"ReactIcons",{enumerable:!0,get:function(){return r.ReactIcons}}),t.HeavyCheckMark="✔️",t.Pencil="✏️",t.TrashCan="🗑️",t.CrossMark="❌",t.Plus="➕",t.Warning="⚠️",t.Spinner="🔄",t.Wrench="🔧",t.Flag="🚩",t.UpArrow="⬆️",t.LeftArrow="⬅",t.RightArrow="➡",t.DownArrow="⬇️",t.DNA="🧬",t.InputNumbers="🔢",t.FourWayArrow="☩",t.MagnifyingGlass="🔍",t.House="🏠",t.Scroll="📜",t.PlayButton="▶️",t.StopButton="⏹️",t.PauseButton="⏸️",t.FastForwardButton="⏩",t.RewindButton="⏪",t.Questionmark="?",t.Pan="🤚",t.Point="👆"},68067:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CsvStringToArray=void 0,t.CsvStringToArray=function(e){for(var t,n,o=/(,|\r?\n|\r|^)(?:"([^"]*(?:""[^"]*)*)"|([^,\r\n]*))/gi,r=[[]];null!=(n=o.exec(e));)n[1].length>0&&","!==n[1]&&r.push([]),r[r.length-1].push(void 0!==n[2]?n[2].replace(/""/g,'"'):n[3]);return 1!==r.length&&1===(null===(t=null==r?void 0:r[r.length-1])||void 0===t?void 0:t.length)&&""===(null==r?void 0:r[r.length-1][0])&&r.pop(),r}},28931:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ComputeMax=function(e){for(var t,n=0,o=e;nt)&&(t=r)}return void 0===t?NaN:t}},44177:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ComputeMin=function(e){for(var t,n=0,o=e;n{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CreateGuid=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)})}},46447:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.FormatDuration=function(e){var t=31556952,n=e/1e3,o=n/t,r="",a=0;o>=1&&(r=Math.floor(o).toFixed(0)+" year"+(o>=2?"s":""));var i=(n-(a=Math.floor(o)*t))/86400;if(i>=1&&(r=r+(r.length>0?" ":"")+Math.floor(i).toFixed(0)+" day"+(i>=2?"s":"")),o>=1)return r;var c=(n-(a+=86400*Math.floor(i)))/3600;if(c>=1&&(r=r+(r.length>0?" ":"")+Math.floor(c).toFixed(0)+" hour"+(c>=2?"s":"")),i>=50)return r;var p=(n-(a+=3600*Math.floor(c)))/60;if(p>=1&&(r=r+(r.length>0?" ":"")+Math.floor(p).toFixed(0)+" minute"+(p>=2?"s":"")),i>=1)return r;var l=n-(a+=60*Math.floor(p));if(l>=1&&(r=r+(r.length>0?" ":"")+Math.floor(l).toFixed(0)+" second"+(l>=2?"s":"")),c>=1)return r;var s=e-1e3*(a+=Math.floor(l));return s>=1&&(r=r+(r.length>0?" ":"")+Math.floor(s).toFixed(0)+" milliseconds"+(s>=2?"s":"")),r}},64093:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.findLastIndex=function(e,t){for(var n=e.length;n-- >0;)if(t(e[n],n,e))return n;return-1}},61681:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.GetColor=t.maxGPAColorIndex=void 0,t.maxGPAColorIndex=21,t.GetColor=function(e){return"var(--gpa-".concat(e%t.maxGPAColorIndex,")")}},7201:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.GetNodeSize=function(e){if(null===e)return{height:0,width:0,top:0,left:0};var t=e.getBoundingClientRect(),n=t.height,o=t.width,r=t.top,a=t.left;return{height:parseInt(n.toString(),10),width:parseInt(o.toString(),10),top:parseInt(r.toString(),10),left:parseInt(a.toString(),10)}}},89418:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){var e=document.createElement("div");e.style.visibility="hidden",e.style.overflow="scroll",document.body.appendChild(e);var t=document.createElement("div");e.appendChild(t);var n=e.offsetWidth-t.offsetWidth;return e.parentNode.removeChild(e),n}},13376:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.GetTextHeight=function(e,t,n,o,r,a){var i=document.createElement("span");void 0!==o&&(i.style.cssText=o),i.style.font=e,i.style.fontSize=t,i.style.height="auto",i.style.width=null!=r?r:"auto",i.style.position="absolute",i.style.whiteSpace=null!=a?a:"no-wrap",i.innerHTML=n,document.body.appendChild(i);var c=Math.ceil(i.clientHeight);return document.body.removeChild(i),c}},72667:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.GetTextWidth=function(e,t,n,o,r,a,i){var c=document.createElement("span");void 0!==o&&(c.style.cssText=o),c.style.font=e,c.style.fontSize=t,c.style.height=null!=r?r:"auto",c.style.width="auto",c.style.whiteSpace=null!=a?a:"nowrap",c.innerHTML=n;var p=document.createElement("div");p.style.position="absolute",p.style.visibility="hidden",p.style.overflow="visible",p.style.height=null!=r?r:"auto",p.style.width=null!=i?i:"auto",p.style.whiteSpace=null!=a?a:"nowrap",p.appendChild(c),document.body.appendChild(p);var l=c.offsetWidth;return document.body.removeChild(p),Math.ceil(l)}},60451:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.HexToHsv=function(e){var t="#"===e[0]?e.slice(1):e;function n(e){return Number("0x"+t.slice(2*e,2*(e+1)))}var o=n(0)/255,r=n(1)/255,a=n(2)/255,i=Math.max(o,r,a),c=Math.min(o,r,a),p=i-c,l=0,s=0==i?0:p/i,u=i;if(i===c)l=0;else{switch(i){case o:l=(r-a)/p+(r{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.HsvToHex=function(e,t,n){var o,r,a,i=Math.floor(6*e),c=6*e-i,p=n*(1-t),l=n*(1-c*t),s=n*(1-(1-c)*t);switch(i%6){case 0:o=n,r=s,a=p;break;case 1:o=l,r=n,a=p;break;case 2:o=p,r=n,a=s;break;case 3:o=p,r=l,a=n;break;case 4:o=s,r=p,a=n;break;default:o=n,r=p,a=l}function u(e){return("00"+Math.floor(255*e).toString(16)).slice(-2)}return"#"+u(o)+u(r)+u(a)}},94598:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.IsBool=void 0,t.IsBool=function(e){if("boolean"==typeof e)return!0;var t=(null!=e?e:"").toString().toLowerCase();return"true"===t||"false"===t}},49906:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.IsCron=function(e){var t="((?:\\*)|(?:({token},)+{token}{1})|(?:{token}-{token})|(?:(?:\\*\\/)?{token}))",n="(^[*]$)|(^"+t.replace(/{token}/g,"[1-5]?[0-9]")+" "+t.replace(/{token}/g,"[1]?[0-9]|2[0-3]")+" "+t.replace(/{token}/g,"[1-9]|[12][0-9]|3[01]")+" "+t.replace(/{token}/g,"[1-9]|1[0-2]")+" "+t.replace(/{token}/g,"[0-6]")+"$)";return null!=e.toString().match(n)}},3350:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.IsInteger=function(e){return null!=e.toString().match(/^-?[0-9]+$/)}},6527:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.IsNumber=function(e){return null!=e.toString().match(/^-?\d+\.?(\d+)?$/)}},55959:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.IsRegex=function(e){try{return new RegExp(e),!0}catch(e){return!1}}},62925:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.JoinKeyValuePairs=void 0;var o=n(94598);t.JoinKeyValuePairs=function(e,t,n,r,a){var i;void 0===t&&(t=";"),void 0===n&&(n="="),void 0===r&&(r="{"),void 0===a&&(a="}");var c=[];for(var p in e)if(Object.prototype.hasOwnProperty.call(e,p)){var l=e[p];((l=(0,o.IsBool)(null!=l?l:"")?l.toString().toLowerCase():null!=l&&null!==(i=null==l?void 0:l.toString())&&void 0!==i?i:"").indexOf(t)>=0||l.indexOf(n)>=0||l.indexOf(r)>=0||l.indexOf(a)>=0)&&(l=r+l+a),c.push(p+n+l)}return c.join(t+" ")}},98430:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ParseKeyValuePairs=void 0;var o=n(59501),r=n(76457);t.ParseKeyValuePairs=function(e,t,n,a,i,c){if(void 0===t&&(t=";"),void 0===n&&(n="="),void 0===a&&(a="{"),void 0===i&&(i="}"),void 0===c&&(c=!0),t===n||t===a||t===i||n===a||n===i||a===i)throw"All delimiters must be unique";for(var p=(0,o.RegexEncode)(t),l=(0,o.RegexEncode)(n),s=(0,o.RegexEncode)(a),u=(0,o.RegexEncode)(i),b=(0,o.RegexEncode)("\\"),M=new Map,d=[],z=!1,O=0,f=0;f0)){z=!1;continue}O--}z?h===t?d.push(p):h===n?d.push(l):h===a?d.push(s):h===i?d.push(u):"\\"===h?d.push(b):d.push(h):"\\"===h?d.push(b):d.push(h)}if(0!==O||z)throw z&&(O=1),"Failed to parse key/value pairs: invalid delimiter mismatch. Encountered more "+(O>0?'start value delimiters "'+a+'"':'end value delimiters "'+i+'"')+" than "+(O<0?'start value delimiters "'+a+'"':'end value delimiters "'+i+'"')+".";var A=d.join("").split(t);for(f=0;f{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.RandomColor=function(){return"#"+Math.random().toString(16).substr(2,6).toUpperCase()}},82479:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.useEffectWithPrevious=function(e,t){var n=o.useRef(void 0);return o.useEffect(function(){var o=e(n.current);return n.current=t,o},[t]),n.current};var o=n(8674)},61407:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.useGetContainerPosition=void 0;var o=n(8674);t.useGetContainerPosition=function(e){var t=o.useState(0),n=t[0],r=t[1],a=o.useState(0),i=a[0],c=a[1],p=o.useState(0),l=p[0],s=p[1],u=o.useState(0),b=u[0],M=u[1],d=o.useState(0),z=d[0],O=d[1],f=o.useState(0),h=f[0],A=f[1],m=o.useState(0),q=m[0],v=m[1],_=o.useState(0),y=_[0],g=_[1],W=o.useState(0),L=W[0],R=W[1],w=o.useState(0),S=w[0],x=w[1],k=o.useState(0),T=k[0],N=k[1],E=o.useState(0),B=E[0],D=E[1];return o.useLayoutEffect(function(){if(null!=e.current){var t=function(){if(null!=e.current){var t=e.current.getBoundingClientRect(),o=e.current.scrollWidth;t.top!==n&&r(t.top),t.left!==i&&c(t.left),t.height!==l&&s(t.height),e.current.scrollHeight!==b&&M(e.current.scrollHeight),e.current.clientHeight!==z&&O(e.current.clientHeight),t.width!==h&&A(t.width),o!==q&&v(o),y!==e.current.clientWidth&&g(e.current.clientWidth),t.x!==L&&R(t.x),t.y!==S&&x(t.y),t.bottom!==T&&N(t.bottom),t.right!==B&&D(t.right)}},o=new ResizeObserver(t);return o.observe(e.current),t(),function(){o.disconnect()}}}),{top:n,left:i,height:l,width:h,x:L,y:S,bottom:T,right:B,scrollWidth:q,clientWidth:y,scrollHeight:b,clientHeight:z}}},23474:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(19639);t.default=function(){var e=(0,o.default)("(min-width: 576px) and (max-width: 767.98px)"),t=(0,o.default)("(min-width: 768px) and (max-width: 991.98px)"),n=(0,o.default)("(min-width: 992px) and (max-width: 1199.98px)");return(0,o.default)("(min-width: 1200px)")?"xl":n?"lg":t?"md":e?"sm":"xs"}},19639:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(8674);t.default=function(e){var t=o.useState(function(){return"undefined"!=typeof window&&null!=window.matchMedia&&window.matchMedia(e).matches}),n=t[0],r=t[1];return o.useEffect(function(){if("undefined"!=typeof window&&null!=window.matchMedia){var t=window.matchMedia(e),n=function(e){return r(e.matches)};return t.addEventListener("change",n),function(){t.removeEventListener("change",n)}}},[e]),n}},59501:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.RegexEncode=void 0,t.RegexEncode=function(e){for(var t=e.charCodeAt(0).toString(16);t.length<4;)t="0"+t;return"\\u"+t}},76457:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ReplaceAll=void 0,t.ReplaceAll=function(e,t,n,o){var r="string"==typeof n?n.replace(/\$/g,"$$$$"):n,a=null!=o&&o?"gi":"g",i=new RegExp(t.replace(/([/\\,!^${}[\]().*+?|<>\-&])/g,"\\$&"),a);return e.replace(i,r)}},93237:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SpacedColor=function(e,t){return r=(r+a)%1,(0,o.HsvToHex)(r,e,t)};var o=n(53699),r=Math.random(),a=.618033988749895},76752:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.FormatDuration=t.ComputeMin=t.ComputeMax=t.RegexEncode=t.ReplaceAll=t.IsBool=t.JoinKeyValuePairs=t.ParseKeyValuePairs=t.useGetScreenSize=t.useMediaQuery=t.GetScrollbarWidth=t.useGetContainerPosition=t.CsvStringToArray=t.IsRegex=t.useEffectWithPrevious=t.findLastIndex=t.HexToHsv=t.HsvToHex=t.SpacedColor=t.IsCron=t.IsInteger=t.IsNumber=t.GetTextHeight=t.RandomColor=t.GetColor=t.GetNodeSize=t.GetTextWidth=t.CreateGuid=void 0;var o=n(26971);Object.defineProperty(t,"CreateGuid",{enumerable:!0,get:function(){return o.CreateGuid}});var r=n(72667);Object.defineProperty(t,"GetTextWidth",{enumerable:!0,get:function(){return r.GetTextWidth}});var a=n(13376);Object.defineProperty(t,"GetTextHeight",{enumerable:!0,get:function(){return a.GetTextHeight}});var i=n(7201);Object.defineProperty(t,"GetNodeSize",{enumerable:!0,get:function(){return i.GetNodeSize}});var c=n(26294);Object.defineProperty(t,"RandomColor",{enumerable:!0,get:function(){return c.RandomColor}});var p=n(6527);Object.defineProperty(t,"IsNumber",{enumerable:!0,get:function(){return p.IsNumber}});var l=n(3350);Object.defineProperty(t,"IsInteger",{enumerable:!0,get:function(){return l.IsInteger}});var s=n(49906);Object.defineProperty(t,"IsCron",{enumerable:!0,get:function(){return s.IsCron}});var u=n(93237);Object.defineProperty(t,"SpacedColor",{enumerable:!0,get:function(){return u.SpacedColor}});var b=n(53699);Object.defineProperty(t,"HsvToHex",{enumerable:!0,get:function(){return b.HsvToHex}});var M=n(60451);Object.defineProperty(t,"HexToHsv",{enumerable:!0,get:function(){return M.HexToHsv}});var d=n(82479);Object.defineProperty(t,"useEffectWithPrevious",{enumerable:!0,get:function(){return d.useEffectWithPrevious}});var z=n(64093);Object.defineProperty(t,"findLastIndex",{enumerable:!0,get:function(){return z.findLastIndex}});var O=n(55959);Object.defineProperty(t,"IsRegex",{enumerable:!0,get:function(){return O.IsRegex}});var f=n(68067);Object.defineProperty(t,"CsvStringToArray",{enumerable:!0,get:function(){return f.CsvStringToArray}});var h=n(61407);Object.defineProperty(t,"useGetContainerPosition",{enumerable:!0,get:function(){return h.useGetContainerPosition}});var A=n(89418);t.GetScrollbarWidth=A.default;var m=n(19639);t.useMediaQuery=m.default;var q=n(23474);t.useGetScreenSize=q.default;var v=n(98430);Object.defineProperty(t,"ParseKeyValuePairs",{enumerable:!0,get:function(){return v.ParseKeyValuePairs}});var _=n(62925);Object.defineProperty(t,"JoinKeyValuePairs",{enumerable:!0,get:function(){return _.JoinKeyValuePairs}});var y=n(94598);Object.defineProperty(t,"IsBool",{enumerable:!0,get:function(){return y.IsBool}});var g=n(76457);Object.defineProperty(t,"ReplaceAll",{enumerable:!0,get:function(){return g.ReplaceAll}});var W=n(59501);Object.defineProperty(t,"RegexEncode",{enumerable:!0,get:function(){return W.RegexEncode}});var L=n(28931);Object.defineProperty(t,"ComputeMax",{enumerable:!0,get:function(){return L.ComputeMax}});var R=n(44177);Object.defineProperty(t,"ComputeMin",{enumerable:!0,get:function(){return R.ComputeMin}});var w=n(46447);Object.defineProperty(t,"FormatDuration",{enumerable:!0,get:function(){return w.FormatDuration}});var S=n(61681);Object.defineProperty(t,"GetColor",{enumerable:!0,get:function(){return S.GetColor}})},14237:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){var t,n,c,p,l=o.useState([]),s=l[0],u=l[1],b=o.useState(null!==(n=null===(t=e.DateTime)||void 0===t?void 0:t.month())&&void 0!==n?n:0),M=b[0],d=b[1],z=o.useState(null!==(p=null===(c=e.DateTime)||void 0===c?void 0:c.year())&&void 0!==p?p:r.utc().year()),O=z[0],f=z[1],h=o.useState("month"),A=h[0],m=h[1];function q(t){var n=r(e.DateTime);n.isValid()||(n=r.utc().startOf("d")),n.year(t.year()).month(t.month()).date(t.date()),e.Setter(n)}o.useEffect(function(){var t,n,o,a,i,c,p,l;d(isNaN(null!==(n=null===(t=e.DateTime)||void 0===t?void 0:t.month())&&void 0!==n?n:NaN)?0:null!==(a=null===(o=e.DateTime)||void 0===o?void 0:o.month())&&void 0!==a?a:0),f(isNaN(null!==(c=null===(i=e.DateTime)||void 0===i?void 0:i.year())&&void 0!==c?c:NaN)?r.utc().year():null!==(l=null===(p=e.DateTime)||void 0===p?void 0:p.year())&&void 0!==l?l:r.utc().year())},[e.DateTime]),o.useEffect(function(){for(var e=r([O,M,1]).startOf("week"),t=[];e.month()<=M&&e.year()===O||e.year()")),"month"===A?o.createElement("tr",{style:{height:20,lineHeight:"20px"}},o.createElement("td",{style:{width:20,padding:5}},"SU"),o.createElement("td",{style:{width:20,padding:5}},"MO"),o.createElement("td",{style:{width:20,padding:5}},"TU"),o.createElement("td",{style:{width:20,padding:5}},"WE"),o.createElement("td",{style:{width:20,padding:5}},"TH"),o.createElement("td",{style:{width:20,padding:5}},"FR"),o.createElement("td",{style:{width:20,padding:5}},"SA")):null),o.createElement("tbody",null,"month"===A?s.map(function(t){return o.createElement("tr",{key:t.sunday.isoWeek(),style:{height:20,lineHeight:"20px"}},o.createElement(a,{date:t.sunday,month:M,dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),q(t.sunday)}}),o.createElement(a,{date:t.monday,month:M,dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),q(t.monday)}}),o.createElement(a,{date:t.tuesday,month:M,dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),q(t.tuesday)}}),o.createElement(a,{date:t.wednesday,month:M,dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),q(t.wednesday)}}),o.createElement(a,{date:t.thursday,month:M,dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),q(t.thursday)}}),o.createElement(a,{date:t.friday,month:M,dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),q(t.friday)}}),o.createElement(a,{date:t.saturday,month:M,dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),q(t.saturday)}}))}):null,"year"===A?o.createElement(o.Fragment,null,o.createElement("tr",{style:{height:54,lineHeight:"54px"}},o.createElement(i,{date:r([O,0,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(0)}}),o.createElement(i,{date:r([O,1,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(1)}}),o.createElement(i,{date:r([O,2,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(2)}}),o.createElement(i,{date:r([O,3,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(3)}})),o.createElement("tr",{style:{height:54,lineHeight:"54px"}},o.createElement(i,{date:r([O,4,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(4)}}),o.createElement(i,{date:r([O,5,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(5)}}),o.createElement(i,{date:r([O,6,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(6)}}),o.createElement(i,{date:r([O,7,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(7)}})),o.createElement("tr",{style:{height:54,lineHeight:"54px"}},o.createElement(i,{date:r([O,8,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(8)}}),o.createElement(i,{date:r([O,9,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(9)}}),o.createElement(i,{date:r([O,10,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(10)}}),o.createElement(i,{date:r([O,11,1]),dateTime:e.DateTime,onClick:function(e){e.stopPropagation(),m("month"),d(11)}}))):null)))};var o=n(8674),r=n(328),a=function(e){var t=o.useState(!1),n=t[0],r=t[1],a=o.useState(!1),i=a[0],c=a[1],p=o.useState(!1),l=p[0],s=p[1];o.useEffect(function(){var t;r(e.date.date()===(null===(t=e.dateTime)||void 0===t?void 0:t.date())&&e.dateTime.month()===e.date.month()&&e.date.year()===e.dateTime.year()),s(e.date.month()!==e.month)},[e.month,e.date,e.dateTime]);var u=l?"#777":n?"#fff":void 0,b=n?"#337ab7":i?"#d3d3d3":void 0;return o.createElement("td",{style:{width:20,padding:5,color:u,backgroundColor:b,cursor:n?"default":"pointer"},onClick:e.onClick,onMouseEnter:function(){return c(!0)},onMouseLeave:function(){return c(!1)}},e.date.format("DD"))},i=function(e){var t=o.useState(!1),n=t[0],r=t[1],a=o.useState(!1),i=a[0],c=a[1];o.useEffect(function(){var t,n;r(e.date.month()===(null===(t=e.dateTime)||void 0===t?void 0:t.month())&&e.date.year()===(null===(n=e.dateTime)||void 0===n?void 0:n.year()))},[e.date,e.dateTime]);var p=n?"#fff":void 0,l=n?"#337ab7":i?"#d3d3d3":void 0;return o.createElement("td",{style:{width:54,padding:5,color:p,backgroundColor:l,cursor:n?"default":"pointer"},onMouseEnter:function(){return c(!0)},onMouseLeave:function(){return c(!1)},onClick:e.onClick},e.date.format("MMM"))}},16151:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){var t,n,a,c,p,l,s,u,b=o.useState(null!==(n=null===(t=e.DateTime)||void 0===t?void 0:t.format("HH"))&&void 0!==n?n:"00"),M=b[0],d=b[1],z=o.useState(null!==(c=null===(a=e.DateTime)||void 0===a?void 0:a.format("mm"))&&void 0!==c?c:"00"),O=z[0],f=z[1],h=o.useState(null!==(l=null===(p=e.DateTime)||void 0===p?void 0:p.format("ss"))&&void 0!==l?l:"00"),A=h[0],m=h[1],q=o.useState(null!==(u=null===(s=e.DateTime)||void 0===s?void 0:s.format("SSS"))&&void 0!==u?u:"000"),v=q[0],_=q[1],y=o.useState("none"),g=y[0],W=y[1];function L(t){var n=r(e.DateTime).add(1,t);"h"===t&&d(n.format("HH")),"m"===t&&f(n.format("mm")),"s"===t&&m(n.format("ss")),"ms"===t&&_(n.format("SSS"))}function R(t){var n=r(e.DateTime).subtract(1,t);"h"===t&&d(n.format("HH")),"m"===t&&f(n.format("mm")),"s"===t&&m(n.format("ss")),"ms"===t&&_(n.format("SSS"))}return o.useEffect(function(){var t,n,o,r,a,i,c,p;d(null!==(n=null===(t=e.DateTime)||void 0===t?void 0:t.format("HH"))&&void 0!==n?n:"00"),f(null!==(r=null===(o=e.DateTime)||void 0===o?void 0:o.format("mm"))&&void 0!==r?r:"00"),m(null!==(i=null===(a=e.DateTime)||void 0===a?void 0:a.format("ss"))&&void 0!==i?i:"00"),_(null!==(p=null===(c=e.DateTime)||void 0===c?void 0:c.format("SSS"))&&void 0!==p?p:"000")},[e.DateTime]),o.useEffect(function(){var t,n,o,a,i=parseInt(M,10),c=parseInt(O,10),p=parseInt(A,10),l=parseInt(v,10);if(!(isNaN(i)||isNaN(c)||isNaN(p)||isNaN(l)||i===(null===(t=e.DateTime)||void 0===t?void 0:t.hour())&&c===(null===(n=e.DateTime)||void 0===n?void 0:n.minute())&&p===(null===(o=e.DateTime)||void 0===o?void 0:o.second())&&l===(null===(a=e.DateTime)||void 0===a?void 0:a.millisecond()))){var s=r(e.DateTime);s.isValid()||(s=r.utc().startOf("d")),s.hour(i).minute(c).second(p).millisecond(l),e.Setter(s)}},[M,O,A,v]),o.createElement("div",{style:{background:"#f0f0f0",marginTop:10,opacity:1}},o.createElement("table",{style:{textAlign:"center"}},o.createElement("tbody",null,o.createElement("tr",{style:{height:20,lineHeight:"20px"}},o.createElement("td",{style:{width:52,padding:5,cursor:"pointer",background:"increase_h"===g?"#d3d3d3":void 0},onClick:function(){return L("h")},onMouseEnter:function(){return W("increase_h")},onMouseLeave:function(){return W("none")}}," ^ "),o.createElement("td",{style:{width:20,padding:5}}),o.createElement("td",{style:{width:52,padding:5,cursor:"pointer",background:"increase_m"===g?"#d3d3d3":void 0},onClick:function(){return L("m")},onMouseEnter:function(){return W("increase_m")},onMouseLeave:function(){return W("none")}}," ^ "),("second"===e.Accuracy||"millisecond"===e.Accuracy)&&o.createElement(o.Fragment,null,o.createElement("td",{style:{width:20,padding:5}}),o.createElement("td",{style:{width:52,padding:5,cursor:"pointer",background:"increase_s"===g?"#d3d3d3":void 0},onClick:function(){return L("s")},onMouseEnter:function(){return W("increase_s")},onMouseLeave:function(){return W("none")}}," ^ ")),"millisecond"===e.Accuracy&&o.createElement(o.Fragment,null,o.createElement("td",{style:{width:20,padding:5}}),o.createElement("td",{style:{width:52,padding:5,cursor:"pointer",background:"increase_ms"===g?"#d3d3d3":void 0},onClick:function(){return L("ms")},onMouseEnter:function(){return W("increase_ms")},onMouseLeave:function(){return W("none")}}," ^ "))),o.createElement("tr",{style:{height:20,lineHeight:"20px"}},o.createElement("td",{style:{width:52,padding:5}}," ",o.createElement(i,{value:M,setValue:d,max:23})," "),o.createElement("td",{style:{width:20,padding:5}}," : "),o.createElement("td",{style:{width:52,padding:5}}," ",o.createElement(i,{value:O,setValue:f,max:59})," "),("second"===e.Accuracy||"millisecond"===e.Accuracy)&&o.createElement(o.Fragment,null,o.createElement("td",{style:{width:20,padding:5}}," : "),o.createElement("td",{style:{width:52,padding:5}}," ",o.createElement(i,{value:A,setValue:m,max:59})," ")),"millisecond"===e.Accuracy&&o.createElement(o.Fragment,null,o.createElement("td",{style:{width:20,padding:5}}," : "),o.createElement("td",{style:{width:52,padding:5}}," ",o.createElement(i,{value:v,setValue:_,max:999})," "))),o.createElement("tr",{style:{height:20,lineHeight:"20px"}},o.createElement("td",{style:{width:52,padding:5,cursor:"pointer",background:"decrease_h"===g?"#d3d3d3":void 0},onClick:function(){return R("h")},onMouseEnter:function(){return W("decrease_h")},onMouseLeave:function(){return W("none")}}," v "),o.createElement("td",{style:{width:20,padding:5}}),o.createElement("td",{style:{width:52,padding:5,cursor:"pointer",background:"decrease_m"===g?"#d3d3d3":void 0},onClick:function(){return R("m")},onMouseEnter:function(){return W("decrease_m")},onMouseLeave:function(){return W("none")}}," v "),("second"===e.Accuracy||"millisecond"===e.Accuracy)&&o.createElement(o.Fragment,null,o.createElement("td",{style:{width:20,padding:5}}),o.createElement("td",{style:{width:52,padding:5,cursor:"pointer",background:"decrease_s"===g?"#d3d3d3":void 0},onClick:function(){return R("s")},onMouseEnter:function(){return W("decrease_s")},onMouseLeave:function(){return W("none")}}," v ")),"millisecond"===e.Accuracy&&o.createElement(o.Fragment,null,o.createElement("td",{style:{width:20,padding:5}}),o.createElement("td",{style:{width:52,padding:5,cursor:"pointer",background:"decrease_ms"===g?"#d3d3d3":void 0},onClick:function(){return R("ms")},onMouseEnter:function(){return W("decrease_ms")},onMouseLeave:function(){return W("none")}}," v "))))))};var o=n(8674),r=n(328),a=n(76752),i=function(e){var t=o.useState(e.value.toString()),n=t[0],r=t[1],i=o.useState(!1),c=i[0],p=i[1];return o.useEffect(function(){e.value.length>5?r(""):r(e.value.toString())},[e.value]),o.useEffect(function(){if((0,a.IsInteger)(n)){var t=parseInt(n,10);t>e.max||t<0?p(!0):(p(!1),e.setValue(n))}},[n,c]),o.createElement("div",{className:"form-group form-group-sm",style:{width:52}},o.createElement("input",{type:"text",className:c?"form-control is-invalid":"form-control",onChange:function(e){(0,a.IsInteger)(e.target.value)&&r(e.target.value)},value:n}))}},62138:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;nM)}},[M,k,$]),r.createElement("div",{className:"form-group",ref:b},J||Z?r.createElement("label",{className:"d-flex align-items-center"},r.createElement("span",null,Z?Q:""),J&&r.createElement("span",{className:"ml-2 d-flex align-items-center",onMouseEnter:function(){return S(!0)},onMouseLeave:function(){return S(!1)},"data-tooltip":L},r.createElement(l.ReactIcons.QuestionMark,{Color:"var(--info)",Size:20}))):null,J?r.createElement(p.default,{Show:w,Target:L,Class:"info",Position:"top"},e.Help):null,r.createElement("input",{className:"gpa-gemstone-datetime form-control ".concat(P.length>0||!e.Valid(e.Field)?"is-invalid":""),type:void 0===e.Type?"date":e.Type,onChange:function(t){!function(t){var n,r,i=K(""===t?void 0:a(t,y));U&&""===t?(e.Setter(o(o({},e.Record),((n={})[e.Field]=null,n))),G(void 0)):i?(e.Setter(o(o({},e.Record),((r={})[e.Field]=a(t,y).format(g),r))),G(a(t,y))):G(void 0),T(t)}(t.target.value)},onFocus:function(){return _(!0)},value:k,disabled:void 0!==e.Disabled&&e.Disabled,onClick:function(e){return e.preventDefault()},step:$,ref:n,"data-tooltip":m,onMouseOver:function(){return A(!0)},onMouseOut:function(){return A(!1)}}),r.createElement("div",{className:"invalid-feedback"},0!=P.length?P:null==e.Feedback||0==e.Feedback.length?"".concat(e.Field.toString()," is a required field."):e.Feedback),r.createElement(p.default,{Show:z&&h,Target:m},null!=e.Format?a(k).format(e.Format):k),r.createElement(i.default,{ref:u,Setter:function(t){G(t),"date"===e.Type&&_(!1)},Show:v,DateTime:E,Top:Y,Center:H,Type:void 0===e.Type?"date":e.Type,Accuracy:e.Accuracy}))},t.getBoxFormat=s;var r=n(8674),a=n(328),i=n(1088),c=n(76752),p=n(40170),l=n(36400);function s(e,t){var n=null!=e?e:"date",o=null!=t?t:"second";return"time"===n?"minute"===o?"HH:mm":"second"===o?"HH:mm:ss":"HH:mm:ss.SSS":"datetime-local"===n?"minute"===o?"YYYY-MM-DD[T]HH:mm":"second"===o?"YYYY-MM-DD[T]HH:mm:ss":"YYYY-MM-DD[T]HH:mm:ss.SSS":"YYYY-MM-DD"}t.getInputWidth=function(e,t,n){var o=window.getComputedStyle(e),r=document.createElement("input");r.type=e.type,r.value=t,r.step=n,r.style.font=o.font,r.style.fontSize=o.fontSize,r.style.fontFamily=o.fontFamily,r.style.fontWeight=o.fontWeight,r.style.letterSpacing=o.letterSpacing,r.style.whiteSpace="nowrap",r.style.position="absolute",r.style.visibility="hidden",document.body.appendChild(r);var a=r.getBoundingClientRect().width;return document.body.removeChild(r),a}},1088:function(e,t,n){"use strict";var o=this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e};Object.defineProperty(t,"__esModule",{value:!0});var r,a=n(8674),i=n(3105),c=n(48081),p=n(36395),l=n(16151),s=n(76752),u=c.default.div(r||(r=o(["\n & {\n border-radius: 3px;\n display: inline-block;\n font-size: 13px;\n padding: 8px 21px;\n position: fixed;\n transition: opacity 0.3s ease-out;\n z-index: 9999;\n opacity: 0.9;\n background: #222;\n top: ",";\n left: ",";\n border: 1px solid transparent;\n }\n &::before {\n border-left: 8px solid transparent;\n border-right: 8px solid transparent;\n border-bottom: 8px solid #222;\n left: ",';\n top: -6px;\n margin-left: -8px;\n content: "";\n width: 0px;\n height: 0px;\n position: absolute\n }'],["\n & {\n border-radius: 3px;\n display: inline-block;\n font-size: 13px;\n padding: 8px 21px;\n position: fixed;\n transition: opacity 0.3s ease-out;\n z-index: 9999;\n opacity: 0.9;\n background: #222;\n top: ",";\n left: ",";\n border: 1px solid transparent;\n }\n &::before {\n border-left: 8px solid transparent;\n border-right: 8px solid transparent;\n border-bottom: 8px solid #222;\n left: ",';\n top: -6px;\n margin-left: -8px;\n content: "";\n width: 0px;\n height: 0px;\n position: absolute\n }'])),function(e){return"".concat(e.Top,"px")},function(e){return"".concat(e.Left,"px")},function(e){return"".concat(e.Indicator,"%")});t.default=a.forwardRef(function(e,t){var n=a.useRef(null),o=a.useState("date"!==e.Type),r=o[0],c=o[1],b=a.useState("time"!==e.Type),M=b[0],d=b[1],z=null!=t?t:n,O=(0,s.useGetContainerPosition)(z).width,f=Math.max(e.Center-.5*O,0);return a.useEffect(function(){c("date"!==e.Type),d("time"!==e.Type)},[e.Type]),e.Show?a.createElement(i.Portal,null,a.createElement(u,{Top:e.Top,Left:f,Indicator:50,ref:z,className:"gpa-gemstone-datetime-popup"},M?a.createElement(p.default,{DateTime:e.DateTime,Setter:e.Setter}):null,r?a.createElement(l.default,{DateTime:e.DateTime,Setter:e.Setter,Accuracy:e.Accuracy}):null)):null})},95678:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(8674),r=n(36400);t.default=function(e){var t=o.useState(null),n=t[0],a=t[1],i=o.useState(null),c=i[0],p=i[1],l=o.useState(!1),s=l[0],u=l[1],b=o.useState("uninitiated"),M=b[0],d=b[1],z=function(t){a(t.name),p(t.size),d("loading"),e.OnLoadHandler(t).then(function(){u(!0),d("idle")}).catch(function(){u(!1),d("error")})};return o.createElement(o.Fragment,null,o.createElement("div",{className:"row"},o.createElement("div",{className:"col-auto mt-2 pl-0"},o.createElement("label",{style:{cursor:"pointer"}},o.createElement(r.ReactIcons.ShareArrow,{Color:"var(--info)"}),o.createElement("input",{type:"file",accept:e.FileTypeAttribute,style:{display:"none"},onChange:function(e){if(null!=e.target&&null!=e.target.files&&0!==e.target.files.length){var t=e.target.files[0];z(t)}else d("uninitiated")}}))),o.createElement("div",{className:"col-auto pl-0"},o.createElement("button",{className:"btn",onClick:function(){d("uninitiated"),u(!1),a(null),p(null),e.OnClearHandler()}},o.createElement(r.ReactIcons.CircledX,{Color:"red"})))),"error"===M?o.createElement("div",{className:"alert alert-danger fade show"},"An error occured while uploading file."):null,o.createElement("div",{className:"row",onDragOver:function(e){e.preventDefault(),e.stopPropagation()},onDrop:function(e){if(e.preventDefault(),e.stopPropagation(),null!=e.dataTransfer&&null!=e.dataTransfer.files&&0!==e.dataTransfer.files.length){var t=e.dataTransfer.files[0];z(t),e.dataTransfer.clearData()}},style:{border:"2px dashed var(--secondary)",borderRadius:"0.5em"}},"loading"===M?o.createElement("div",{className:"d-flex col-12 align-items-center justify-content-center"},o.createElement(r.ReactIcons.SpiningIcon,{Size:200})):s?o.createElement(o.Fragment,null,o.createElement("div",{className:"col-auto"},"File Name: ",null!=n?n:""),o.createElement("div",{className:"col-auto"},"File Size: ",function(e){if(null===e)return"";for(var t=["bytes","KB","MB","GB","TB"],n=0,o=e;o>=1024&&n0&&"."===n[0]?"0"+n:n;(0,c.IsNumber)(u)||""===u&&s?(e.Setter(r(r({},e.Record),((a={})[e.Field]=""!==u?parseFloat(u):null,a))),O(u)):""===u&&O(u)}else"integer"===e.Type?(0,c.IsInteger)(n)||""===n&&s?(e.Setter(r(r({},e.Record),((i={})[e.Field]=""!==n?parseFloat(n):null,i))),O(n)):""===n&&O(n):("text"===e.Type&&null!==(l=e.AllowNull)&&void 0!==l&&l&&o.warn("Input component: Empty strings are set to null for Type='text' and AllowNull=true to maintain current functionality."),e.Setter(r(r({},e.Record),((p={})[e.Field]=""!==n?n:null,p))),O(n))}(n.target.value)},value:z,disabled:null!=e.Disabled&&e.Disabled,onBlur:function(){var n,o;void 0!==e.AllowNull&&e.AllowNull||"number"!==e.Type&&"integer"!==e.Type||""!==z||(t.current=!1,e.Setter(r(r({},e.Record),((n={})[e.Field]=null!==(o=e.DefaultValue)&&void 0!==o?o:0,n))))},step:"any"}),a.createElement("div",{className:"invalid-feedback"},null==e.Feedback?e.Field.toString()+" is a required field.":e.Feedback))};var a=n(8674),i=n(40170),c=n(76752),p=n(36400)},75165:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n0&&"."===n[0]?"0"+n:n;(0,i.IsNumber)(l)||""===l&&p?(e.Setter(o(o({},e.Record),((r={})[e.Field]=""!==l?parseFloat(l):null,r))),z(l)):""===l&&z(l)}else"integer"===e.Type?(0,i.IsInteger)(n)||""===n&&p?(e.Setter(o(o({},e.Record),((a={})[e.Field]=""!==n?parseFloat(n):null,a))),z(n)):""===n&&z(n):(e.Setter(o(o({},e.Record),((c={})[e.Field]=""!==n?n:null,c))),z(n))}(n.target.value)},value:d,disabled:null!=e.InputDisabled&&e.InputDisabled,onBlur:function(){var n,r;void 0!==e.AllowNull&&e.AllowNull||"number"!==e.Type&&"integer"!==e.Type||""!==d||(t.current=!1,e.Setter(o(o({},e.Record),((n={})[e.Field]=null!==(r=e.DefaultValue)&&void 0!==r?r:0,n))))},step:"any"}),r.createElement("div",{className:"input-group-prepend"},r.createElement("button",{className:null!=e.BtnClass?e.BtnClass:"btn btn-outline-secondary",style:e.BtnStyle,disabled:null!=e.BtnDisabled&&e.BtnDisabled,type:"button",onClick:function(t){return e.OnBtnClick(t)}},e.BtnLabel)),r.createElement("div",{className:"invalid-feedback"},null==e.Feedback?e.Field.toString()+" is a required field.":e.Feedback)))}},8458:function(e,t,n){"use strict";var o=n(4364),r=this&&this.__assign||function(){return r=Object.assign||function(e){for(var t,n=1,o=arguments.length;n{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(76752),r=n(8674),a=n(40170),i=n(3105),c=n(42005),p=n(36400);t.default=function(e){var t,n=r.useRef(null),l=r.useRef(null),s=r.useRef(null),u=r.useState(!1),b=u[0],M=u[1],d=r.useState(!1),z=d[0],O=d[1],f=r.useState(!1),h=f[0],A=f[1],m=r.useState((0,o.CreateGuid)())[0],q=r.useState((0,o.CreateGuid)())[0],v=r.useMemo(function(){return""!==e.Label},[e.Label]),_=r.useMemo(function(){return void 0!==e.Help},[e.Help]),y=r.useMemo(function(){return e.Options.filter(function(e){return e.Selected})},[e.Options]),g=r.useState({Top:0,Left:0,Width:0,Height:0}),W=g[0],L=g[1];function R(e){null!=l.current&&l.current.contains(e.target)||null!=l.current&&l.current.contains(e.target)||null!=s.current&&s.current.contains(e.target)||(null===n.current?M(!b):n.current.contains(e.target)?M(!0):M(!1))}return r.useEffect(function(){var e=c.debounce(function(){if(null!=n.current){var e=n.current.getBoundingClientRect();L({Top:e.bottom,Left:e.left,Width:e.width,Height:e.height})}},200),t=function(t){null!=s.current&&("scroll"!==t.type||s.current.contains(t.target)||M(!1),e())};if(b)return e(),window.addEventListener("scroll",t,!0),window.addEventListener("resize",e),function(){window.removeEventListener("scroll",t,!0),window.removeEventListener("resize",e),e.cancel()}},[b]),r.useEffect(function(){return document.addEventListener("mousedown",R,!1),function(){document.removeEventListener("mousedown",R,!1)}},[]),r.createElement("div",{className:"form-group"},v||_?r.createElement("label",{className:"d-flex align-items-center"},r.createElement("span",null,v?void 0===e.Label?"Select":e.Label:""),_?r.createElement("span",{className:"ml-2 d-flex align-items-center",onMouseEnter:function(){return O(!0)},onMouseLeave:function(){return O(!1)},"data-tooltip":q},r.createElement(p.ReactIcons.QuestionMark,{Color:"var(--info)",Size:20})):null):null,_?r.createElement(a.default,{Show:z,Target:q,Class:"info",Position:"top"},e.Help):null,null!==(t=e.ShowToolTip)&&void 0!==t&&t?r.createElement(a.default,{Show:h,Target:m,Position:"top"},r.createElement("p",null,"Selected Options:"),y.slice(0,10).map(function(e,t){return r.createElement("p",{key:t},e.Label)}),y.length>10?r.createElement("p",null,"and ".concat(y.length-10," other(s)")):null):null,r.createElement("div",{ref:n,style:{position:"relative",display:"block",width:"inherit"}},r.createElement("button",{"data-tooltip":m,type:"button",style:{padding:".375rem .75rem",fontSize:"1rem",color:"currentColor",backgroundColor:"inherit"},className:"btn border form-control dropdown-toggle",onClick:R,onMouseEnter:function(){return A(!0)},onMouseLeave:function(){return A(!1)}},e.Options.filter(function(e){return e.Selected}).length!==e.Options.length?e.Options.filter(function(e){return e.Selected}).length:"All "," "," Selected"),r.createElement(i.Portal,null,r.createElement("div",{ref:s,className:"popover",style:{maxHeight:window.innerHeight-W.Top,overflowY:"auto",padding:"10 5",display:b?"block":"none",position:"absolute",color:"currentColor",zIndex:9999,top:"".concat(W.Top,"px"),left:"".concat(W.Left,"px"),width:"".concat(W.Width,"px"),maxWidth:"100%"}},r.createElement("table",{className:"table table-hover",style:{margin:0},ref:l},r.createElement("tbody",null,r.createElement("tr",{onClick:function(t){t.preventDefault(),e.OnChange(t,e.Options.filter(function(t){return t.Selected===(e.Options.filter(function(e){return e.Selected}).length===e.Options.length)}))}},r.createElement("td",null,r.createElement("input",{type:"checkbox",checked:e.Options.filter(function(e){return e.Selected}).length===e.Options.length,onChange:function(){return null}})),r.createElement("td",null,"All")),e.Options.map(function(t,n){return r.createElement("tr",{key:n,onClick:function(n){return e.OnChange(n,[t])}},r.createElement("td",null,r.createElement("input",{type:"checkbox",checked:t.Selected,onChange:function(){return null}})),r.createElement("td",null,t.Label))})))))))}},40789:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){var t=o.useState(function(){var t,n,o;return i(null!==(t=e.ResetSearchOnSelect)&&void 0!==t&&t,null!==(o=null===(n=e.Record[e.Field])||void 0===n?void 0:n.toString())&&void 0!==o?o:"")}),n=t[0],c=t[1],p=o.useState([]),l=p[0],s=p[1],u=o.useState(!1),b=u[0],M=u[1],d=o.useCallback(function(t,n){var o=n;z(o),e.Setter(t,{Label:o.Label,Value:o.Value})},[e.Setter,e.Field]),z=o.useCallback(function(t){var n,o,r,a;if(null!==(n=e.ResetSearchOnSelect)&&void 0!==n&&n)c("");else if(void 0!==e.GetLabel)M(!0),e.GetLabel().then(function(e){c(e),M(!1)},function(){return M(!1)});else{var i=null!==(a=null!==(o=null==t?void 0:t.Label)&&void 0!==o?o:null===(r=e.Record[e.Field])||void 0===r?void 0:r.toString())&&void 0!==a?a:"";c(i)}},[e.ResetSearchOnSelect,e.GetLabel,e.Record[e.Field]]);o.useEffect(function(){z()},[e.Record[e.Field],z]),o.useEffect(function(){var t;M(!0);var o=setTimeout(function(){(t=e.Search(n)).then(function(e){s(e.map(function(e){return{Value:e.Value,Element:e.Label,Label:e.Label}})),M(!1)},function(){M(!1)})},500);return function(){null!=(null==t?void 0:t.abort)&&t.abort(),null!=o&&clearTimeout(o)}},[n]);var O=o.useMemo(function(){var t,r,i,p,s=[];return s.push({Value:e.Record[e.Field],Label:"",Element:o.createElement("div",{className:"input-group"},o.createElement("input",{type:"text",className:"form-control ".concat(null===(r=null===(t=e.Valid)||void 0===t?void 0:t.call(e,e.Field))||void 0===r||r?"":"border-danger"),value:n,onChange:function(e){return c(e.target.value)},onBlur:function(){return z()},onClick:function(e){e.preventDefault(),e.stopPropagation()},disabled:null!==(i=e.Disabled)&&void 0!==i&&i}),b?o.createElement("div",{className:"input-group-append"},o.createElement("span",{className:"input-group-text"},o.createElement(a.ReactIcons.SpiningIcon,null))):null)}),null!==(p=e.AllowCustom)&&void 0!==p&&p&&s.push({Value:n,Element:o.createElement(o.Fragment,null,n," (Entered Value)"),Label:n}),s.push.apply(s,l.filter(function(t){return t.Value!==n&&t.Value!==e.Record[e.Field]})),s},[n,e.Record[e.Field],e.Field,l,e.Disabled,b,e.Valid,z]);return o.createElement(r.default,{Record:e.Record,Field:e.Field,Setter:d,Label:e.Label,Disabled:e.Disabled,Help:e.Help,Style:e.Style,Options:O,BtnStyle:e.BtnStyle,Valid:e.Valid,Feedback:e.Feedback})};var o=n(8674),r=n(41887),a=n(36400),i=function(e,t){return e?"":t}},73557:function(e,t,n){"use strict";var o=n(4364),r=this&&this.__assign||function(){return r=Object.assign||function(e){for(var t,n=1,o=arguments.length;n0&&-1===e.Options.findIndex(function(e){return e.Value==n})){var r=e.Options[0];b(r.Value,r),o.warn("The current value is not available as an option. Specify EmptyOption=true if the value should be allowed.")}},[e.Options]);var d=""!==e.Label,z=void 0!==e.Help,O=void 0===e.Label?e.Field:e.Label;return a.createElement("div",{className:"form-group"},z||d?a.createElement("label",{className:"d-flex align-items-center"},a.createElement("span",null,d?O:""),z&&a.createElement("span",{className:"ml-2 d-flex align-items-center",onMouseEnter:function(){return u(!0)},onMouseLeave:function(){return u(!1)},"data-tooltip":n},a.createElement(p.ReactIcons.QuestionMark,{Color:"var(--info)",Size:20}))):null,z?a.createElement(i.default,{Show:s,Target:n,Class:"info",Position:"top"},e.Help):null,a.createElement("select",{className:"form-control",onChange:function(t){var n=t.target.value,o=e.Options.find(function(e){return e.Value==n});b(n,o)},value:M(),disabled:null!=e.Disabled&&e.Disabled},null!==(t=e.EmptyOption)&&void 0!==t&&t?a.createElement("option",{value:""},void 0!==e.EmptyLabel?e.EmptyLabel:""):null,e.Options.map(function(e,t){return a.createElement("option",{key:t,value:e.Value},e.Label)})))};var a=n(8674),i=n(40170),c=n(76752),p=n(36400)},41887:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n=o.height+5?(l="top",r=t.Top-o.height-5):(l="bottom",r=t.Top+t.Height+5),(a=t.Left+.5*t.Width-.5*o.width)+o.width>c&&(a=c-o.width-5),a<0&&(a=5)):"bottom"===n&&(p-(t.Top+t.Height)>=o.height+5?(l="bottom",r=t.Top+t.Height+5):(l="top",r=t.Top-o.height-5),(a=t.Left+.5*t.Width-.5*o.width)+o.width>=c&&(a=c-o.width-5),a<=0&&(a=5));var s=50;return"top"===n||"bottom"===n?s=(t.Left+.5*t.Width-a)/o.width*100:"left"!==n&&"right"!==n||(s=(t.Top+.5*t.Height-r)/o.height*100),[r,a,s,l]};t.default=t.Tooltip},35225:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.MultiSearchableSelect=t.ToolTip=t.MultiInput=t.FileUpload=t.RadioButtons=t.InputWithButton=t.ToggleSwitch=t.SearchableSelect=t.ColorPicker=t.StylableSelect=t.TimePicker=t.DoubleInput=t.MultiCheckBoxSelect=t.ArrayCheckBoxes=t.ArrayMultiSelect=t.EnumCheckBoxes=t.DateRangePicker=t.TextArea=t.Select=t.DatePicker=t.Input=t.CheckBox=void 0;var o=n(92564);t.CheckBox=o.default;var r=n(42793);t.Input=r.default;var a=n(62138);t.DatePicker=a.default;var i=n(73557);t.Select=i.default;var c=n(27559);t.TextArea=c.default;var p=n(95378);t.DateRangePicker=p.default;var l=n(97573);t.EnumCheckBoxes=l.default;var s=n(55963);t.ArrayMultiSelect=s.default;var u=n(14237);t.ArrayCheckBoxes=u.default;var b=n(52221);t.MultiCheckBoxSelect=b.default;var M=n(95678);t.DoubleInput=M.default;var d=n(29810);t.TimePicker=d.default;var z=n(41887);t.StylableSelect=z.default;var O=n(76348);t.ColorPicker=O.default;var f=n(51297);t.SearchableSelect=f.default;var h=n(7021);t.ToggleSwitch=h.default;var A=n(75165);t.InputWithButton=A.default;var m=n(40789);t.RadioButtons=m.default;var q=n(69430);t.FileUpload=q.default;var v=n(8458);t.MultiInput=v.default;var _=n(40170);t.ToolTip=_.default;var y=n(684);t.MultiSearchableSelect=y.default},83904:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var i=a(n(8674));t.default=function(e){var t,n,o=i.useState(!0),r=o[0],a=o[1],c=i.useCallback(function(t){a(!1),null!=e.OnClick&&e.OnClick(t)},[e.OnClick]);return i.useEffect(function(){a(!0)},[e.ReTrigger]),i.createElement("div",{className:"alert ".concat(null!==(t=e.Class)&&void 0!==t?t:"alert-dark"," alert-dismissible fade ").concat(r?"show":"d-none"),style:e.Style},e.children,null===(n=e.ShowX)||void 0===n||n?i.createElement("button",{type:"button",className:"close",onClick:c},i.createElement("span",{"aria-hidden":"true"},"×")):null)}},11997:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var i=a(n(8674));t.default=function(e){return i.createElement("nav",null,i.createElement("ol",{className:"breadcrumb"},e.Steps.map(function(t,n){var o;return i.createElement("li",{key:"".concat(t.ID).concat(n),className:"breadcrumb-item ".concat(e.CurrentStep.ID===t.ID?"active":"")},t.ID===e.CurrentStep.ID||null!==(o=t.IsNavigable)&&void 0!==o&&!o?t.Label:i.createElement("a",{href:"#",onClick:function(n){n.preventDefault(),e.OnClick(t)}},t.Label))})))}},9089:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var i=a(n(8674)),c=function(e){return null==e.Options||0===e.Options.length};t.default=function(e){var t,n,o=i.useState(e.Data.RootId),r=o[0],a=o[1],p=e.Data.Nodes[r];return i.useEffect(function(){c(p)&&null!=e.OnComplete&&e.OnComplete(p.RecommendedValue)},[p]),i.useEffect(function(){a(e.Data.RootId)},[e.ResetToRoot]),null==p?null!==(t=e.FallbackHelp)&&void 0!==t?t:i.createElement(i.Fragment,null):i.createElement("div",{className:"d-flex flex-column align-items-center justify-content-center"},"string"==typeof p.Prompt?i.createElement("p",{className:"mb-2"},p.Prompt):p.Prompt,c(p)?null:i.createElement("div",{className:"mb-2"},(null!==(n=p.Options)&&void 0!==n?n:[]).map(function(e){return i.createElement("button",{key:e.NextNodeKey,className:"btn btn-primary mr-2",onClick:function(){return a(e.NextNodeKey)}},e.Label)})))}},39545:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n0&&e.Options[n].Group!==e.Options[n-1].Group?c.createElement("div",{className:"dropdown-divider"}):null,c.createElement(s,o({},t,{setShowDropDown:m})))})),c.createElement(l.ToolTip,{Show:O&&null!==(a=e.ShowToolTip)&&void 0!==a&&a,Position:null!==(i=e.TooltipLocation)&&void 0!==i?i:"top",Target:u.current},e.TooltipContent))}},12447:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n0&&r[r.length-1])||6!==c[0]&&2!==c[0])){i=0;continue}if(3===c[0]&&(!r||c[1]>r[0]&&c[1]0||(t.Status="error",t.Error={Message:null==n.error.message?"":n.error.message,Verb:"FETCH",Time:(new Date).toString()},null!==a.actionErrorDependency&&a.actionErrorDependency(t,"".concat(e,"/Fetch").concat(e),n.meta.arg,n.meta.requestId))}),t.addCase(d.pending,function(t,n){t.Status="loading",null!==a.actionPendingDependency&&a.actionPendingDependency(t,"".concat(e,"/DBAction").concat(e),n.meta.arg,n.meta.requestId)}),t.addCase(d.rejected,function(t,n){t.Status="error",t.Error={Message:null==n.error.message?"":n.error.message,Verb:n.meta.arg.verb,Time:(new Date).toString()},null!==a.actionErrorDependency&&a.actionErrorDependency(t,"".concat(e,"/DBAction").concat(e),n.meta.arg,n.meta.requestId)}),t.addCase(d.fulfilled,function(t,n){t.Status="changed",t.SearchStatus="changed",t.Error=null,null!==a.actionFullfilledDependency&&a.actionFullfilledDependency(t,"".concat(e,"/DBAction").concat(e),n.meta.arg,n.meta.requestId)}),t.addCase(z.pending,function(t,n){t.SearchStatus="loading",t.ActiveSearchID.push(n.meta.requestId),null!==a.actionPendingDependency&&a.actionPendingDependency(t,"".concat(e,"/Search").concat(e),n.meta.arg,n.meta.requestId)}),t.addCase(z.rejected,function(t,n){t.ActiveSearchID=t.ActiveSearchID.filter(function(e){return e!==n.meta.requestId}),t.ActiveSearchID.length>0||(t.SearchStatus="error",t.Error={Message:null==n.error.message?"":n.error.message,Verb:"SEARCH",Time:(new Date).toString()},null!==a.actionErrorDependency&&a.actionErrorDependency(t,"".concat(e,"/Search").concat(e),n.meta.arg,n.meta.requestId))}),t.addCase(z.fulfilled,function(t,n){t.ActiveSearchID=t.ActiveSearchID.filter(function(e){return e!==n.meta.requestId}),t.SearchStatus="idle",t.SearchResults=n.payload,t.Filter=n.meta.arg.filter,null!==a.actionFullfilledDependency&&a.actionFullfilledDependency(t,"".concat(e,"/Search").concat(e),n.meta.arg,n.meta.requestId)}),t.addCase(f.pending,function(t,n){t.PagedStatus="loading",t.ActivePagedID.push(n.meta.requestId),null!==a.actionPendingDependency&&a.actionPendingDependency(t,"".concat(e,"/Page").concat(e),n.meta.arg,n.meta.requestId)}),t.addCase(f.rejected,function(t,n){t.ActivePagedID=t.ActivePagedID.filter(function(e){return e!==n.meta.requestId}),t.ActivePagedID.length>0||(t.PagedStatus="error",t.Error={Message:null==n.error.message?"":n.error.message,Verb:"PAGE",Time:(new Date).toString()},null!==a.actionErrorDependency&&a.actionErrorDependency(t,"".concat(e,"/Page").concat(e),n.meta.arg,n.meta.requestId))}),t.addCase(f.fulfilled,function(t,n){t.ActivePagedID=t.ActivePagedID.filter(function(e){return e!==n.meta.requestId}),t.PagedStatus="idle",t.TotalPages=n.payload.NumberOfPages,t.SearchResults=JSON.parse(n.payload.Data),null!=n.meta.arg.filter&&(t.Filter=n.meta.arg.filter),void 0!==n.meta.arg.page&&(t.CurrentPage=n.meta.arg.page),t.TotalRecords=n.payload.TotalRecords,null!==a.actionFullfilledDependency&&a.actionFullfilledDependency(t,"".concat(e,"/Page").concat(e),n.meta.arg,n.meta.requestId)}),t.addCase(O.pending,function(t,n){t.Status="loading",t.ActiveFetchID.push(n.meta.requestId),null!==a.actionPendingDependency&&a.actionPendingDependency(t,"".concat(e,"/DBSort").concat(e),n.meta.arg,n.meta.requestId)}),t.addCase(O.rejected,function(t,n){t.ActiveFetchID=t.ActiveFetchID.filter(function(e){return e!==n.meta.requestId}),t.ActiveFetchID.length>0||(t.Status="error",t.Error={Message:null==n.error.message?"":n.error.message,Verb:"FETCH",Time:(new Date).toString()},null!==a.actionErrorDependency&&a.actionErrorDependency(t,"".concat(e,"/DBSort").concat(e),n.meta.arg,n.meta.requestId))}),t.addCase(O.fulfilled,function(t,n){t.ActiveFetchID=t.ActiveFetchID.filter(function(e){return e!==n.meta.requestId}),t.Status="idle",t.Error=null,t.Data=n.payload,t.SortField===n.meta.arg.SortField?t.Ascending=!t.Ascending:t.SortField=n.meta.arg.SortField,null!==a.actionFullfilledDependency&&a.actionFullfilledDependency(t,"".concat(e,"/DBSort").concat(e),n.meta.arg,n.meta.requestId)}),t.addCase(h.pending,function(e){e.Status="changed",e.SearchStatus="changed"}),b(t)}});this.AdditionalThunk=p,this.Fetch=M,this.DBAction=d,this.Slice=A,this.DBSearch=z,this.PagedSearch=f,this.Sort=O,this.Reducer=A.reducer,this.SetChanged=h}},61678:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.AllHelperAlertKeys=t.ConfigurableTableKey=void 0,t.ConfigurableTableKey="".concat("Gemstone.HelperAlerts.","ConfigurableTable"),t.AllHelperAlertKeys=[t.ConfigurableTableKey]},82797:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.ConfigurableTableAlert=void 0;var c=a(n(8674)),p=i(n(83904)),l=n(61678);t.ConfigurableTableAlert=function(){var e=c.useState(!0),t=e[0],n=e[1];return c.useEffect(function(){"true"===localStorage.getItem(l.ConfigurableTableKey)&&n(!1)},[]),c.createElement(c.Fragment,null,t?c.createElement(p.default,{OnClick:function(){localStorage.setItem(l.ConfigurableTableKey,"true")},Class:"alert-primary"},"Use the gear at the far right of the header row to choose columns to show or hide."):null)}},95357:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Gemstone=void 0;var o,r=n(61678),a=n(82797);!function(e){var t;(t=e.HelperAlerts||(e.HelperAlerts={})).ConfigurableTable=a.ConfigurableTableAlert,t.ResetAllHelperAlertValues=function(){return r.AllHelperAlertKeys.forEach(function(e){return localStorage.setItem(e,"false")})}}(o||(t.Gemstone=o={}))},50418:function(e,t,n){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var r=o(n(8674));t.default=function(e){var t=r.default.useState([]),n=t[0],o=t[1],a=r.default.Children.count(e.children);return r.default.useEffect(function(){var t=[],n=Math.floor(Math.sqrt(a)),r=n,i=a-Math.pow(n,2);if(void 0!==e.ColMax&&e.ColMax<=n){i=0;var c=e.ColMax;r=Math.ceil(a/c)}a>=Math.pow(n,2)+n&&(i-=n,r=n+1);for(var p=a,l=0;l=0;++l){var s={StartIndex:a-p,NumOfCols:c=n+1};p-=c,t.push(s)}for(l=0;l=0;++l)c=n===e.ColMax?e.ColMax:n,s={StartIndex:a-p,NumOfCols:Math.min(p,c)},p-=c,t.push(s);o(t)},[a,e.ColMax]),r.default.createElement("div",{className:"container-fluid p-0 h-100",style:{overflowY:"auto"}},n.map(function(t,o){return r.default.createElement("div",{key:o,className:"row pb-1 m-0",style:{height:"".concat(100/Math.min(n.length,e.RowsPerPage),"%")}},function(t){for(var n=[],o=0;o0?e:100)},[m]),b.useEffect(function(){var e=m.reduce(function(e,t){var n;return e+(!t.IsDrawer||null!==(n=t.Open)&&void 0!==n&&n?1:0)},0)-1,t=0;M.some(function(e){return void 0===e.props.ShowClosed||e.props.ShowClosed})&&(t=35),t+=m.reduce(function(e,t){var n;return e+(t.IsDrawer&&null!==(n=t.Open)&&void 0!==n&&n?35:0)},0),L(r-t-5*e)},[r,m]),b.useEffect(function(){var t,n,o,r;l(null!==(n=null===(t=b.Children.map(e.children,function(e){return b.isValidElement(e)&&e.type===z.default?e:null}))||void 0===t?void 0:t.filter(function(e){return null!==e}))&&void 0!==n?n:[]),h(null!==(r=null===(o=b.Children.map(e.children,function(e){return b.isValidElement(e)&&e.type===d.default?e:null}))||void 0===o?void 0:o.filter(function(e){return null!==e}))&&void 0!==r?r:[])},[e.children]),b.useEffect(function(){var e=c([],m,!0),t=!1;M.forEach(function(n,o){var r=e.find(function(e){return e.Index===o&&e.IsDrawer}),a={Width:n.props.Width,MinWidth:n.props.MinWidth,MaxWidth:n.props.MaxWidth,Open:n.props.Open,Index:o,IsDrawer:!0,ShowClosed:n.props.ShowClosed,Percentage:n.props.Width,Label:n.props.Title,Order:o-M.length,GetOverride:n.props.GetOverride};if(void 0===r)return t=!0,void e.push(a);N(r,a)||(r.Label=a.Label,r.MaxWidth=a.MaxWidth,r.MinWidth=a.MinWidth,r.Percentage=a.Width,r.ShowClosed=a.ShowClosed,r.GetOverride=a.GetOverride,r.Width>a.MaxWidth&&(r.Width=a.MaxWidth),r.Widtha.MaxWidth&&(r.Width=a.MaxWidth),r.Width=M.length||!e.IsDrawer&&e.Index>=p.length})&&(t=!0,e=e.filter(function(e){return!(e.IsDrawer&&e.Index>=M.length||!e.IsDrawer&&e.Index>=p.length)})),t&&q(e)},[M,p]),b.useEffect(function(){m.forEach(function(e,t){void 0!==e.GetOverride&&e.GetOverride(function(t){t!==e.Open&&E(e.Index)})})},[m]);var B,D,P,C=m.some(function(e){return e.IsDrawer&&(void 0===e.ShowClosed||e.ShowClosed)});return b.createElement("div",{className:"d-flex",style:o({},e.style),ref:t,onMouseUp:function(){return S(-1)},onMouseMove:function(e){if(!(w<0)){var t=e.clientX-k,n=t/(W/_);if(!(t<10&&t>-10)){T(e.clientX);var o=u.orderBy(m,function(e){return e.Order}).filter(function(e){return!e.IsDrawer||e.Open}),r=c([],m,!0),a=o.reduce(function(e,t,n){return e+(n<=w?t.MinWidth:0)},0),i=o.reduce(function(e,t,n){return e+(n<=w?t.MaxWidth:0)},0),p=o.reduce(function(e,t,n){return e+(n<=w?t.Width:0)},0),l=o.reduce(function(e,t,n){return e+(n<=w?0:t.MinWidth)},0),s=o.reduce(function(e,t,n){return e+(n<=w?0:t.MaxWidth)},0),b=o.reduce(function(e,t,n){return e+(n<=w?0:t.Width)},0);p+ni&&(n=i-p),b-ns&&(n=b-s);for(var M=n,d=w;0!==M&&d>=0;){var z=r.find(function(e){return e.Index===o[d].Index&&e.IsDrawer===o[d].IsDrawer});d-=1,void 0!==z&&(z.Width+Mz.MaxWidth?(M-=z.MaxWidth-z.Width,z.Width=z.MaxWidth):(z.Width=z.Width+M,M=0))}for(M=-(n-M),d=w+1;0!==M&&dz.MaxWidth?(M-=z.MaxWidth-z.Width,z.Width=z.MaxWidth):(z.Width=z.Width+M,M=0));q(r)}}},onMouseLeave:function(){return S(-1)}},C?b.createElement("div",{style:{width:35,float:"left",minHeight:1,height:"100%",display:"flex",flexDirection:"column"}},m.map(function(e){var t;return e.IsDrawer&&(void 0===e.ShowClosed||e.ShowClosed)?b.createElement(f,{showTooltip:!0!==e.Open,title:e.Label,symbol:null!==(t=e.Open)&&void 0!==t&&t?"Close":"Open",onClick:function(){return E(e.Index)},key:M[e.Index].key}):null})):null,(B=[],D=W/_,P=0,u.orderBy(m,function(e){return e.Order}).forEach(function(t,n){var o=Math.floor(D*t.Width),r=p[t.Index],a=M[t.Index];if(!t.IsDrawer||!0===t.Open){t.IsDrawer&&null!=a?B.push(b.createElement("div",{style:{width:isNaN(o)?0:o,float:"left",minHeight:1,height:"100%"},key:"draw-"+a.key},a)):null!=r&&B.push(b.createElement("div",{style:{width:isNaN(o)?0:o,float:"left",minHeight:1,height:"100%"},key:"sec-"+r.key},r)),t.IsDrawer&&null!=a&&B.push(b.createElement("div",{style:{width:35,float:"left",minHeight:1,height:"100%"},key:null==a?void 0:a.key},b.createElement(f,{title:t.Label,symbol:void 0===t.ShowClosed||t.ShowClosed?"Close":"X",onClick:function(){return E(t.Index)},showTooltip:!1})));var i=1*P;(t.IsDrawer&&null!=a||!t.IsDrawer&&null!=r)&&B.push(b.createElement(O,{style:e.sliderStyle,onClick:function(e){T(e),S(i)},key:"split-"+(t.IsDrawer?a.key:r.key)})),P+=1}}),B.length>1&&B.pop(),B))};var O=function(e){var t=void 0===e.style?{float:"left",background:"#6c757d",cursor:"col-resize"}:e.style;return b.createElement("div",{style:o({width:5,height:"100%",userSelect:"none",MozUserSelect:"none",WebkitUserSelect:"none"},t),onMouseDown:function(t){return e.onClick(t.clientX)}})},f=function(e){var t=b.useState(!1),n=t[0],o=t[1],r=b.useState((0,s.CreateGuid)()),a=r[0];return r[1],b.createElement(b.Fragment,null,b.createElement("div",{style:{background:"#f8f9fa",cursor:"pointer",zIndex:1e3,width:"100%",height:"100%",overflow:"hidden",flex:1},"data-tooltip":a+"-tooltip",onMouseDown:function(){e.onClick()},onMouseEnter:function(){return o(!0)},onMouseLeave:function(){return o(!1)}},b.createElement("div",{style:{height:24,width:35,paddingLeft:5}},"Open"===e.symbol?b.createElement(l.ReactIcons.ArrowForward,null):null,"Close"===e.symbol?b.createElement(l.ReactIcons.ArrowBackward,null):null,"X"===e.symbol?b.createElement(l.ReactIcons.CrossMark,null):null),b.createElement("span",{style:{margin:"auto",writingMode:"vertical-rl",textOrientation:"sideways",fontSize:25,paddingTop:"5px"}},e.title)),e.showTooltip?b.createElement(M.ToolTip,{Show:n,Position:"right",Target:a+"-tooltip",Zindex:9999},e.title):null)}},44811:function(e,t,n){"use strict";var o=this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e},r=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),a=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&r(t,e,n);return a(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var c,p,l=i(n(8674)),s=i(n(48081)),u=(0,s.keyframes)(c||(c=o(["\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n"],["\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n"]))),b=s.default.div(p||(p=o(["\n\tanimation: "," 1s linear infinite;\n\tborder: ","px solid #f3f3f3;\n\tborder-Top: ","px solid #555;\n\tborder-Radius: 50%;\n\twidth: ","px;\n\theight: ","px\n"],["\n\tanimation: "," 1s linear infinite;\n\tborder: ","px solid #f3f3f3;\n\tborder-Top: ","px solid #555;\n\tborder-Radius: 50%;\n\twidth: ","px;\n\theight: ","px\n"])),u,function(e){return e.size/5},function(e){return e.size/5},function(e){return e.size},function(e){return e.size});t.default=function(e){var t=void 0===e.Size?25:e.Size;return l.createElement("div",null,l.createElement("div",{style:{width:void 0===e.Label?t:void 0,margin:"auto"},hidden:!e.Show},l.createElement(b,{size:t}),void 0!==e.Label?l.createElement("span",null,e.Label):null))}},8822:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var c=a(n(8674)),p=i(n(44811));t.default=function(e){var t=window.innerHeight/2-20;return e.Show?c.createElement("div",{style:{width:"100%",height:"100%",position:"fixed",top:0,left:0,opacity:.5,backgroundColor:"#000000",zIndex:9980}},c.createElement("div",{style:{height:"40px",width:"40px",margin:"auto",marginTop:t}},c.createElement(p.default,{Show:!0,Size:40}))):null}},8036:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var i=a(n(8674)),c=n(76902),p=n(76752);n(65983),t.default=function(e){var t=i.useRef(null),n=(0,p.useGetContainerPosition)(t),o=n.width,r=n.height,a=i.useMemo(function(){return JSON.stringify(e.MapOptions)},[e.MapOptions]),l=i.useMemo(function(){return JSON.stringify(e.TileLayerOptions)},[e.TileLayerOptions]),s=i.useMemo(function(){return JSON.parse(a)},[a]),u=i.useMemo(function(){return JSON.parse(l)},[l]);return i.useEffect(function(){null!=e.Map.current&&e.Map.current.invalidateSize()},[o,r]),i.useEffect(function(){if(null!=t.current)return e.Map.current=(0,c.map)(t.current,s),(0,c.tileLayer)(e.TileLayerURL,u).addTo(e.Map.current),function(){null!=e.Map.current&&(e.Map.current.remove(),e.Map.current=null)}},[s,e.TileLayerURL,u]),i.createElement("div",{className:"h-100 w-100",ref:t})}},60432:function(e,t,n){"use strict";var o=this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e},r=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),a=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&r(t,e,n);return a(t,e),t},c=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var p,l=n(26592),s=i(n(8674)),u=n(85695),b=c(n(19320)),M=c(n(49012)),d=c(n(8822)),z=c(n(35620)),O=c(n(48081)),f=c(n(55110)),h=n(76752),A=c(n(32653)),m=c(n(31280)),q=O.default.div(p||(p=o(["\n& {\n top: ","px;\n position: absolute;\n width: calc(100% - ","px);\n height: calc(100% - ","px);\n overflow: hidden;\n left: ","px;\n}\n& svg {\n user-select: none;\n }"],["\n& {\n top: ","px;\n position: absolute;\n width: calc(100% - ","px);\n height: calc(100% - ","px);\n overflow: hidden;\n left: ","px;\n}\n& svg {\n user-select: none;\n }"])),function(e){return e.top},function(e){return e.left},function(e){return e.top},function(e){return e.left});t.default=s.forwardRef(function(e,t){var n,o=s.useState(!1),r=o[0],a=o[1],i=s.useRef(null),c=s.useRef(null),p=(0,h.useGetContainerPosition)(c).width,O=s.useState(null),v=O[0],_=O[1],y=s.useMemo(function(){return document.title},[]),g=s.useMemo(function(){return null!=v?v:y},[v,y]);s.useEffect(function(){document.title=g},[g]);var W=s.useReducer(function(e){return e+1},0),L=(W[0],W[1]),R=s.useState(40),w=R[0],S=R[1],x=s.useState(50),k=x[0],T=x[1],N=s.useState(!1),E=N[0],B=N[1],D=s.useState(!1),P=D[0],C=D[1],j=s.useState(""),Y=j[0],X=j[1],F=(void 0!==e.AllowCollapsed&&e.AllowCollapsed||P)&&r,H=(void 0!==e.AllowCollapsed&&e.AllowCollapsed||P)&&!r,I=!(void 0===e.HideSideBar&&!E)&&(null!==(n=e.HideSideBar)&&void 0!==n&&n||E);function U(){var t,n;return{homePath:e.HomePath,userRoles:null!==(t=e.UserRoles)&&void 0!==t?t:["Viewer"],collapsed:r,useSearchMatch:null!==(n=e.UseLegacyNavigation)&&void 0!==n&&n,activeSection:Y,setActiveSection:X,setActivePageLabel:_}}function G(t){var n=[];if(void 0!==t.props.RequiredRoles&&0===t.props.RequiredRoles.filter(function(e){return U().userRoles.findIndex(function(t){return t===e})>-1}).length?n.push(s.createElement(u.Route,{path:"".concat(e.HomePath).concat(t.props.Name),element:s.createElement(z.default,{Show:!0,Label:"You are not authorized to view this page."})})):n.push(s.createElement(u.Route,{path:"".concat(e.HomePath).concat(t.props.Name),element:s.createElement(f.default,null,t.props.children)})),null!=t.props.Paths)for(var o=0,r=t.props.Paths;o-1}).length?n.push(s.createElement(u.Route,{path:i,element:s.createElement(z.default,{Show:!0,Label:"You are not authorized to view this page."})})):n.push(s.createElement(u.Route,{path:i,element:s.createElement(f.default,null,t.props.children)}))}return n}return s.useLayoutEffect(function(){var e,t;S(null!==(t=null===(e=i.current)||void 0===e?void 0:e.offsetHeight)&&void 0!==t?t:40)}),s.useEffect(function(){var t;0!==p&&(B(p<=200),p<=600&&(null!==(t=e.AllowCollapsed)&&void 0!==t&&t||C(!0),a(!0)))},[p,e.AllowCollapsed]),s.useEffect(function(){var e=function(e){return L()};return window.addEventListener("resize",e),function(){return window.removeEventListener("resize",e)}},[]),s.useImperativeHandle(t,function(){return{mainDiv:c.current,navBarDiv:i.current}}),s.createElement(l.Context.Provider,{value:U()},void 0!==e.UseLegacyNavigation&&e.UseLegacyNavigation?s.createElement("div",{ref:c,style:{width:"100%",height:"100%",position:"absolute"}},s.createElement(m.default,{SetCollapsed:a,HomePath:e.HomePath,Logo:e.Logo,OnSignOut:e.OnSignOut,ShowOpen:F,ShowClose:H,NavBarContent:e.NavBarContent,ref:i}),s.createElement(A.default,{Collapsed:r,HideSide:I,Version:e.Version,SetSideBarWidth:T,SidebarUI:e.SidebarUI,NavbarHeight:w},e.children),s.createElement(q,{left:I?0:r?k:200,top:w},s.Children.map(e.children,function(e){return s.isValidElement(e)?e.type===b.default&&s.Children.count(e.props.children)>0?e.props.children:e.type===M.default?null:e:null}))):s.createElement(u.BrowserRouter,null,s.createElement("div",{ref:c,style:{width:"100%",height:"100%",position:"absolute"}},s.createElement(m.default,{SetCollapsed:a,HomePath:e.HomePath,Logo:e.Logo,OnSignOut:e.OnSignOut,ShowOpen:!I&&F,ShowClose:!I&&H,NavBarContent:e.NavBarContent,ref:i}),s.createElement(s.Suspense,{fallback:s.createElement(d.default,{Show:!0})},s.createElement(A.default,{Collapsed:r,HideSide:I,Version:e.Version,SidebarUI:e.SidebarUI,SetSideBarWidth:T,NavbarHeight:w},e.children),s.createElement(q,{left:I?0:r?k:200,top:w},s.createElement(u.Routes,null,s.createElement(u.Route,{path:"".concat(e.HomePath)},s.createElement(u.Route,{index:!0,element:s.createElement(u.Navigate,{to:"".concat(e.HomePath).concat(e.DefaultPath)})}),s.Children.map(e.children,function(e){return s.isValidElement(e)?e.type===b.default&&s.Children.count(e.props.children)>0?G(e):e.type===M.default?s.Children.map(e.props.children,function(e){return s.isValidElement(e)&&e.type===b.default&&s.Children.count(e.props.children)>0?G(e):null}):null:null}))))))))})},31280:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var i=a(n(8674)),c=n(36400),p=i.forwardRef(function(e,t){return i.createElement(i.Fragment,null,i.createElement("nav",{className:"navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow",ref:t},e.ShowOpen?i.createElement("a",{style:{color:"var(--light)",marginLeft:15,cursor:"pointer"},onClick:function(){return e.SetCollapsed(!1)}},i.createElement(c.ReactIcons.ArrowForward,null)):null,e.ShowClose?i.createElement("a",{style:{color:"var(--light)",marginLeft:15,cursor:"pointer"},onClick:function(){return e.SetCollapsed(!0)}},i.createElement(c.ReactIcons.ArrowBackward,null)):null,void 0!==e.Logo?i.createElement("a",{className:"navbar-brand col-sm-2 col-md-1 mr-0 mr-auto",href:e.HomePath},i.createElement("img",{style:{maxHeight:35,margin:-5},src:e.Logo})):null,i.createElement("ul",{className:"navbar-nav px-3 ml-auto"},i.createElement("li",{className:"nav-item text-nowrap",style:{cursor:void 0!==e.OnSignOut?"pointer":"default"}},void 0!==e.OnSignOut?i.createElement("a",{className:"nav-link",onClick:e.OnSignOut},"Sign out"):null)),e.NavBarContent))});t.default=p},32653:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n-1}).length?null:null!=e.Label||null!=e.Icon?c.createElement(c.Fragment,null,c.createElement("li",{className:"nav-item",style:{position:"relative"}},c.createElement(p.NavLink,{"data-tooltip":e.Name,className:function(){return a},to:"".concat(u.homePath).concat(e.Name),onMouseEnter:function(){return o(!0)},onMouseLeave:function(){return o(!1)}},void 0!==e.Icon?e.Icon:null,u.collapsed?null:c.createElement("span",null,e.Label))),u.collapsed?c.createElement(i.ToolTip,{Target:e.Name,Show:n,Position:"right"},e.Label):null):null}},49012:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var i=a(n(8674)),c=n(26592),p=n(36400),l=n(76752);t.default=function(e){var t=i.useContext(c.Context),n=i.useState(!0),o=n[0],r=n[1],a=i.useState(!0),s=a[0],u=a[1],b=i.useState((0,l.CreateGuid)()),M=b[0],d=(b[1],i.useMemo(function(){return s?o?i.createElement(p.ReactIcons.ChevronUp,{Style:{width:"1em",height:"1em"}}):i.createElement(p.ReactIcons.ChevronDown,{Style:{width:"1em",height:"1em"}}):null},[s,o]));i.useEffect(function(){var n;null==e.Label||t.collapsed||M===t.activeSection?u(!1):u(null===(n=e.AllowCollapse)||void 0===n||n)},[t.collapsed,t.activeSection,e.Label,e.AllowCollapse]);var z=i.useCallback(function(e){s&&(r(function(e){return!e}),e.preventDefault(),e.stopPropagation())},[s]);return null!=e.RequiredRoles&&0===e.RequiredRoles.filter(function(e){return t.userRoles.findIndex(function(t){return t===e})>-1}).length?null:i.createElement(c.SectionContext.Provider,{value:M},i.createElement("hr",null),null==e.Label||t.collapsed?null:i.createElement(i.Fragment,null,i.createElement("h6",{className:"sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted",onClick:z,style:{cursor:s?"pointer":void 0}},i.createElement("span",null,e.Label," ",d))),o||!s?i.createElement("ul",{className:"navbar-nav px-3",style:e.Style},e.children):null)}},22407:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var i=a(n(8674)),c=n(90782),p=n(76752),l=n(3105);t.default=function(e){var t,n,o,r,a,s,u,b,M,d,z,O,f,h,A,m,q,v=i.useState("none"),_=v[0],y=v[1],g=i.useState(""),W=g[0],L=g[1];return i.useEffect(function(){L((0,p.CreateGuid)())},[]),i.createElement(l.Portal,null,i.createElement("div",{className:"modal"+(e.Show?" show":""),style:e.Show?{display:"block",zIndex:null!==(t=e.ZIndex)&&void 0!==t?t:9990}:{}},i.createElement("div",{className:"modal-dialog"+(void 0===e.Size||"xlg"===e.Size?"":" modal-"+e.Size),style:"xlg"===e.Size?{maxWidth:window.innerWidth-100}:{}},i.createElement("div",{className:"modal-content"},i.createElement("div",{className:"modal-header",style:null!==(n=e.HeaderStyle)&&void 0!==n?n:{}},i.createElement("h4",{className:"modal-title"},e.Title),null!==(o=e.ShowX)&&void 0!==o&&o?i.createElement("button",{type:"button",className:"close",onClick:function(){return e.CallBack(!1,!1,!1)}},"×"):null),i.createElement("div",{className:"modal-body",style:null!==(r=e.BodyStyle)&&void 0!==r?r:{maxHeight:"calc(100vh - 210px)",overflowY:"auto"}},e.Show?e.children:null),null==e.ShowConfirm||e.ShowConfirm||null==e.ShowCancel||e.ShowCancel||null==e.ShowTertiary||e.ShowTertiary?i.createElement("div",{className:"modal-footer"},void 0===e.ShowConfirm||e.ShowConfirm?i.createElement("button",{type:"button",className:"btn ".concat(null!==(a=e.ConfirmBtnClass)&&void 0!==a?a:"btn-primary"," ").concat(null!==(s=e.DisableConfirm)&&void 0!==s&&s?"disabled":""),"data-tooltip":W+"-confirm",onClick:function(){void 0!==e.DisableConfirm&&e.DisableConfirm||e.CallBack(!0,!0,!1)},onMouseEnter:function(){return y("confirm")},onMouseLeave:function(){return y("none")}},null!==(u=e.ConfirmText)&&void 0!==u?u:"Save"):null,null!==(b=e.ShowTertiary)&&void 0!==b&&b?i.createElement("button",{type:"button",className:"btn ".concat(null!==(M=e.TertiaryBtnClass)&&void 0!==M?M:"btn-secondary"," ").concat(null!==(d=e.DisableTertiary)&&void 0!==d&&d?"disabled":""),"data-tooltip":W+"-tertiary",onClick:function(){var t;null!==(t=e.DisableTertiary)&&void 0!==t&&t||e.CallBack(!1,!0,!0)},onMouseEnter:function(){return y("tertiary")},onMouseLeave:function(){return y("none")}},null!==(z=e.TertiaryText)&&void 0!==z?z:"Action"):null,void 0===e.ShowCancel||e.ShowCancel?i.createElement("button",{type:"button",className:"btn ".concat(null!==(O=e.CancelBtnClass)&&void 0!==O?O:"btn-danger"," ").concat(null!==(f=e.DisableCancel)&&void 0!==f&&f?"disabled":""),"data-tooltip":W+"-cancel",onClick:function(){void 0!==e.DisableCancel&&e.DisableCancel||e.CallBack(!1,!0,!1)},onMouseEnter:function(){return y("cancel")},onMouseLeave:function(){return y("none")}},null!==(h=e.CancelText)&&void 0!==h?h:"Cancel"):null):null))),i.createElement(c.ToolTip,{Show:null!==(A=e.ConfirmShowToolTip)&&void 0!==A&&A&&"confirm"===_,Position:"top",Target:W+"-confirm",Zindex:9999},e.ConfirmToolTipContent),i.createElement(c.ToolTip,{Show:null!==(m=e.TertiaryShowToolTip)&&void 0!==m&&m&&"tertiary"===_,Position:"top",Target:W+"-tertiary",Zindex:9999},e.TertiaryToolTipContent),i.createElement(c.ToolTip,{Show:null!==(q=e.CancelShowToolTip)&&void 0!==q&&q&&"cancel"===_,Position:"top",Target:W+"-cancel",Zindex:9999},e.CancelToolTipContent),e.Show?i.createElement("div",{style:{width:"100%",height:"100%",position:"fixed",top:0,left:0,opacity:.5,backgroundColor:"#ffffff",zIndex:9980}}):null)}},48158:function(e,t,n){"use strict";var o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,o=arguments.length;n0&&void 0!==e.defaultCollumn?setTimeout(function(){void 0!==e.defaultCollumn&&w({FieldName:e.defaultCollumn.key,Operator:"LIKE",Type:e.defaultCollumn.type,SearchText:"*"+g+"*",IsPivotColumn:e.defaultCollumn.isPivotField})},500):setTimeout(function(){w(null)},500),function(){null!==t&&clearTimeout(t)}},[g]),l.useEffect(function(){S.current&&null!=e.StorageID||(null!=R?e.SetFilter(c(c([],A,!0),[R],!1)):e.SetFilter(A)),S.current=!1},[R]);var x=l.createElement(l.Fragment,null,l.createElement("form",null,l.createElement("div",{className:"row"},void 0!==e.defaultCollumn?l.createElement("div",{className:"col"},l.createElement("div",{className:"input-group"},l.createElement("input",{className:"form-control mr-sm-2",type:"search",placeholder:"Search "+e.defaultCollumn.label,onChange:function(e){return W(e.target.value)},value:g}),void 0!==e.ShowLoading&&e.ShowLoading?l.createElement("div",{className:"input-group-append"}," ",l.createElement(u.default,{Show:!0})," "):null),l.createElement("p",{style:{marginTop:2,marginBottom:2}},e.ResultNote)):null,l.createElement("div",{style:{position:"relative",display:"inline-block"},className:"col"},l.createElement("button",{className:"btn btn-"+(A.length>0?"warning":"primary"),onClick:function(t){t.preventDefault(),p(!i),f(!0),_({FieldName:e.CollumnList[0].key,SearchText:"",Operator:"string"===e.CollumnList[0].type?"LIKE":"=",Type:e.CollumnList[0].type,IsPivotColumn:e.CollumnList[0].isPivotField})},onMouseEnter:function(){return r(!0)},onMouseLeave:function(){return r(!1)}},"Add Filter",A.length>0?"("+A.length+")":""),l.createElement("div",{className:"popover",style:{display:n?"block":"none",maxWidth:"unset",right:"right"===e.Direction?0:"unset",left:"left"===e.Direction?0:"unset",top:"unset"},onMouseEnter:function(){return r(!0)},onMouseLeave:function(){return r(!1)}},l.createElement("table",{className:"table table-hover"},l.createElement("thead",null,l.createElement("tr",null,l.createElement("th",null,"Column"),l.createElement("th",null,"Operator"),l.createElement("th",null,"Search Text"),l.createElement("th",null,"Edit"),l.createElement("th",null,"Remove"))),l.createElement("tbody",null,A.map(function(t,n){return l.createElement(z,{Filter:t,Edit:function(){return function(t){f(!1);var n=c([],A,!0),r=o({},n[t]);n.splice(t,1),"string"!==r.Type||"LIKE"!==r.Operator&&"NOT LIKE"!==r.Operator||(r.SearchText=r.SearchText.substr(1,r.SearchText.length-2)),p(!0),m(n),_(r),void 0!==e.defaultCollumn&&null!==R?e.SetFilter(c(c([],n,!0),[R],!1)):e.SetFilter(n)}(n)},Delete:function(){return function(t){var n=A.findIndex(function(e){return e===t}),o=c([],A,!0);o.splice(n,1),m(o),r(!1),void 0!==e.defaultCollumn&&null!==R?e.SetFilter(c(c([],o,!0),[R],!1)):e.SetFilter(o)}(t)},key:n,Collumns:e.CollumnList})}))))))));return l.createElement("div",{className:"w-100"},l.createElement("nav",{className:"navbar navbar-expand"},l.createElement("div",{className:"w-100"},l.createElement("ul",{className:"navbar-nav mr-auto d-flex align-items-center w-100"},"right"===e.Direction?e.children:null,void 0!==e.Label?l.createElement("li",{className:"nav-item",style:{minWidth:void 0===e.Width?"150px":void 0,width:e.Width,paddingRight:10}},l.createElement("fieldset",{className:"border",style:{padding:"10px",height:"100%"}},l.createElement("legend",{className:"w-auto",style:{fontSize:"large"}},e.Label,":"),x)):l.createElement("li",{className:"nav-item",style:{minWidth:void 0===e.Width?"150px":void 0,width:e.Width,paddingRight:10}},x),"left"===e.Direction?e.children:null))),l.createElement(s.default,{Title:"Add Filter",Show:i,CallBack:function(t){var n,r;t&&(n=c([],A,!0),"string"!==(r=o({},v)).Type||"LIKE"!==r.Operator&&"NOT LIKE"!==r.Operator||(r.SearchText="*"+r.SearchText+"*"),n.push(r),m(n),_({FieldName:e.CollumnList[0].key,SearchText:"",Operator:"string"===e.CollumnList[0].type?"LIKE":"=",Type:e.CollumnList[0].type,IsPivotColumn:e.CollumnList[0].isPivotField}),void 0!==e.defaultCollumn&&null!==R?e.SetFilter(c(c([],n,!0),[R],!1)):e.SetFilter(n)),p(!1)},ConfirmText:O?"Add":"Save",CancelText:O?"Close":"Delete"},l.createElement(b.Select,{Record:v,Field:"FieldName",Options:e.CollumnList.map(function(e){return{Value:e.key,Label:e.label}}),Setter:function(t){var n="IN",r=e.CollumnList.find(function(e){return e.key===t.FieldName});void 0!==r&&"string"===r.type&&(n="LIKE"),void 0===r||"number"!==r.type&&"integer"!==r.type&&"boolean"!==r.type||(n="="),void 0!==r&&"datetime"===r.type&&(n=">"),_(function(e){return o(o({},e),{FieldName:t.FieldName,SearchText:"",Operator:n,Type:void 0!==r?r.type:"string",IsPivotColumn:void 0===r||r.isPivotField})})},Label:"Column"}),l.createElement(d,{Filter:v,Field:e.CollumnList.find(function(e){return e.key===v.FieldName}),Setter:function(e){return _(e)},Enum:void 0===e.GetEnum?void 0:e.GetEnum})))};var l=i(n(8674)),s=p(n(22407)),u=p(n(44811)),b=n(90782),M=n(36400);function d(e){var t=l.useState([]),n=t[0],r=t[1];if(l.useEffect(function(){if(void 0!==e.Field){if(void 0!==e.Field.enum&&r(e.Field.enum),void 0!==e.Enum)return e.Enum(r,e.Field);void 0===e.Field.enum&&r([])}},[e.Field,e.Enum]),void 0===e.Field)return null;if("string"===e.Field.type)return l.createElement(l.Fragment,null,l.createElement("label",null,"Column type is string. Wildcard (*) can be used with 'LIKE' and 'NOT LIKE'"),l.createElement("div",{className:"row"},l.createElement("div",{className:"col-4"},l.createElement("select",{className:"form-control",value:e.Filter.Operator,onChange:function(t){var n=t.target.value;e.Setter(function(e){return o(o({},e),{Operator:n})})}},l.createElement("option",{value:"LIKE"},"LIKE"),l.createElement("option",{value:"="},"="),l.createElement("option",{value:"NOT LIKE"},"NOT LIKE"))),l.createElement("div",{className:"col"},l.createElement("input",{className:"form-control",value:e.Filter.SearchText.replace("$_","_"),onChange:function(t){var n=t.target.value;e.Setter(function(e){return o(o({},e),{SearchText:n.replace("_","$_")})})}}))));if("integer"===e.Field.type||"number"===e.Field.type)return l.createElement(l.Fragment,null,l.createElement("label",null,"Column type is ",e.Field.type,"."),l.createElement("div",{className:"row"},l.createElement("div",{className:"col-4"},l.createElement("select",{className:"form-control",value:e.Filter.Operator,onChange:function(t){var n=t.target.value;e.Setter(function(e){return o(o({},e),{Operator:n})})}},l.createElement("option",{value:"="},"="),l.createElement("option",{value:"<>"},"<>"),l.createElement("option",{value:">"},">"),l.createElement("option",{value:">="},">="),l.createElement("option",{value:"<"},"<"),l.createElement("option",{value:"<="},"<="))),l.createElement("div",{className:"col"},l.createElement("input",{type:"number",className:"form-control",value:e.Filter.SearchText,onChange:function(t){var n=t.target.value;e.Setter(function(e){return o(o({},e),{SearchText:n})})}}))));if("datetime"===e.Field.type)return l.createElement(l.Fragment,null,l.createElement("label",null,"Column type is ",e.Field.type,"."),l.createElement("div",{className:"row"},l.createElement("div",{className:"col-4"},l.createElement("select",{className:"form-control",value:e.Filter.Operator,onChange:function(t){var n=t.target.value;e.Setter(function(e){return o(o({},e),{Operator:n})})}},l.createElement("option",{value:">"},">"),l.createElement("option",{value:">="},">="),l.createElement("option",{value:"<"},"<"),l.createElement("option",{value:"<="},"<="))),l.createElement("div",{className:"col"},l.createElement(b.DatePicker,{Record:e.Filter,Field:"SearchText",Setter:function(t){var n=t.SearchText;e.Setter(function(e){return o(o({},e),{SearchText:n})})},Label:"",Type:"datetime-local",Valid:function(){return!0},Format:"MM/DD/YYYY HH:mm:ss.SSS"}))));if("boolean"===e.Field.type)return l.createElement("div",{className:"form-check"},l.createElement("input",{type:"checkbox",className:"form-check-input",style:{zIndex:1},onChange:function(t){e.Setter(function(e){return o(o({},e),{Operator:"=",SearchText:t.target.checked?"1":"0"})})},value:"1"===e.Filter.SearchText?"on":"off",checked:"1"===e.Filter.SearchText}),l.createElement("label",{className:"form-check-label"},"Column type is boolean. Yes/On is checked."));var a=function(e){return(null!=e.match(/^\(.*\)$/)?e.slice(1,-1):e).split(",")};return l.createElement(l.Fragment,null,l.createElement("label",null,"Column type is enumerable. Select from below."),l.createElement("ul",{style:{listStyle:"none"}},l.createElement("li",null,l.createElement("div",{className:"form-check"},l.createElement("input",{type:"checkbox",className:"form-check-input",style:{zIndex:1},onChange:function(t){t.target.checked?e.Setter(function(e){return o(o({},e),{SearchText:"(".concat(n.map(function(e){return e.Value}).join(","),")")})}):e.Setter(function(e){return o(o({},e),{SearchText:""})})},defaultValue:"off"}),l.createElement("label",{className:"form-check-label"},"Select All"))),n.map(function(t,n){return l.createElement("li",{key:n},l.createElement("div",{className:"form-check"},l.createElement("input",{type:"checkbox",className:"form-check-input",style:{zIndex:1},onChange:function(n){if(n.target.checked){(i=(i=a(e.Filter.SearchText)).filter(function(e){return""!==e})).push(t.Value);var r="(".concat(i.join(","),")");e.Setter(function(e){return o(o({},e),{SearchText:r})})}else{var i;i=(i=(i=a(e.Filter.SearchText)).filter(function(e){return""!==e})).filter(function(e){return e!==t.Value});var c="(".concat(i.join(","),")");e.Setter(function(e){return o(o({},e),{SearchText:c})})}},value:e.Filter.SearchText.indexOf(t.Value)>=0?"on":"off",checked:a(e.Filter.SearchText).indexOf(t.Value)>=0}),l.createElement("label",{className:"form-check-label"},t.Label)))})))}function z(e){var t=e.Collumns.find(function(t){return t.key===e.Filter.FieldName});return l.createElement("tr",null,l.createElement("td",null,void 0===t?e.Filter.FieldName:t.label),l.createElement("td",null,e.Filter.Operator),l.createElement("td",null,e.Filter.SearchText),l.createElement("td",null,l.createElement("button",{type:"button",className:"btn btn-sm",onClick:function(t){return e.Edit()}},l.createElement("span",null,l.createElement(M.ReactIcons.Pencil,null)))),l.createElement("td",null,l.createElement("button",{type:"button",className:"btn btn-sm",onClick:function(t){return e.Delete()}},l.createElement("span",null,l.createElement(M.ReactIcons.TrashCan,{Style:{color:"var(--danger)"}})))))}t.GetStoredFilters=function(e){var t,n,o=null!==(t=JSON.parse(localStorage.getItem("".concat(e,".Filters"))))&&void 0!==t?t:[],r=null!==(n=localStorage.getItem("".concat(e,".Search")))&&void 0!==n?n:"";return c(c([],o,!0),[r],!1)}},35620:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var i=a(n(8674));t.default=function(e){var t=void 0===e.Size?25:e.Size;return i.createElement("div",null,i.createElement("div",{style:{width:void 0===e.Label?t:void 0,margin:"auto",display:"flex",alignItems:"center"},hidden:!e.Show},i.createElement("svg",{style:{fill:"darkred",height:t},viewBox:"0 0 20 20"},i.createElement("path",{d:"M10.185,1.417c-4.741,0-8.583,3.842-8.583,8.583c0,4.74,3.842,8.582,8.583,8.582S18.768,14.74,18.768,10C18.768,5.259,14.926,1.417,10.185,1.417 M10.185,17.68c-4.235,0-7.679-3.445-7.679-7.68c0-4.235,3.444-7.679,7.679-7.679S17.864,5.765,17.864,10C17.864,14.234,14.42,17.68,10.185,17.68 M10.824,10l2.842-2.844c0.178-0.176,0.178-0.46,0-0.637c-0.177-0.178-0.461-0.178-0.637,0l-2.844,2.841L7.341,6.52c-0.176-0.178-0.46-0.178-0.637,0c-0.178,0.176-0.178,0.461,0,0.637L9.546,10l-2.841,2.844c-0.178,0.176-0.178,0.461,0,0.637c0.178,0.178,0.459,0.178,0.637,0l2.844-2.841l2.844,2.841c0.178,0.178,0.459,0.178,0.637,0c0.178-0.176,0.178-0.461,0-0.637L10.824,10z"})),i.createElement("div",{style:{fontSize:"".concat(t/2,"px"),whiteSpace:"pre-line"}},void 0!==e.Label?e.Label:"A Server Error Occurred. Please Reload the Application."," ")))}},49096:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var i=a(n(8674));t.default=function(e){var t;return i.createElement("div",{className:"progress ".concat(null!==(t=e.Class)&&void 0!==t?t:"w-75")},i.createElement("div",{className:"progress-bar progress-bar-striped progress-bar-animated",role:"progressbar","aria-valuenow":e.CurrentPercentage,"aria-valuemin":0,"aria-valuemax":100,style:{width:"".concat(e.CurrentPercentage,"%")}},null==e.children?"".concat(e.CurrentPercentage,"%"):e.children))}},49272:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0});var i=a(n(8674)),c=n(76752);t.default=function(e){var t=i.useRef(null),n=(0,c.useGetContainerPosition)(t).width,o=i.useState(1),r=o[0],a=o[1],p=i.useState(!1),l=p[0],s=p[1];i.useEffect(function(){s(!1)},[e.CurrentTab]),i.useEffect(function(){for(var t=40,o=0;on-2)break;t+=r,o+=1}a(o)},[n,e.Tabs]);var u=rr-1?null:i.createElement("li",{className:"nav-item",key:n,style:{cursor:"pointer"}},i.createElement("a",{className:"nav-link"+(e.CurrentTab===t.Id?" active":""),onClick:function(){return e.SetTab(t.Id)}},t.Label))}),u?i.createElement("li",{className:"nav-item dropdown ".concat(l?" show":""),style:{zIndex:1040}},i.createElement("a",{className:"nav-link dropdown-toggle",onClick:function(){return s(function(e){return!e})}},"..."),i.createElement("div",{className:"dropdown-menu dropdown-menu-right ".concat(l?" show":"")},e.Tabs.map(function(t,n){return n>r-1?i.createElement("a",{className:"dropdown-item ".concat(e.CurrentTab===t.Id?" active":""),style:{cursor:"pointer"},onClick:function(){return e.SetTab(t.Id)},key:n},t.Label):null}))):null))}},46306:function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n);var r=Object.getOwnPropertyDescriptor(t,n);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,o,r)}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),a=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&o(t,e,n);return r(t,e),t},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var c=a(n(8674)),p=i(n(22407));t.default=function(e){var t;return c.createElement(p.default,{Title:e.Title,Show:e.Show,CancelBtnClass:"btn-danger",CancelText:"Cancel",ConfirmBtnClass:"btn-success",ConfirmText:"Confirm",ShowX:!1,ShowCancel:null===(t=e.ShowCancel)||void 0===t||t,Size:"sm",CallBack:function(t){return e.CallBack(t)},ZIndex:e.ZIndex},c.createElement("p",null,e.Message))}},62382:function(e,t,n){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.Map=t.DecisionHelpTree=t.ExternalPage=t.StatusProgressBar=t.Gemstone=t.Breadcrumb=t.Alert=t.LayoutGrid=t.GenericController=t.BtnDropdown=t.ProgressBar=t.OverlayDrawer=t.SplitDrawer=t.VerticalSplit=t.SplitSection=t.Section=t.Page=t.Application=t.GenericSlice=t.ServerErrorIcon=t.TabSelector=t.ToolTip=t.LoadingIcon=t.LoadingScreen=t.GetStoredFilters=t.SearchBar=t.Warning=t.Modal=void 0;var r=o(n(22407));t.Modal=r.default;var a=o(n(46306));t.Warning=a.default;var i=o(n(75895));t.SearchBar=i.default;var c=n(75895);Object.defineProperty(t,"GetStoredFilters",{enumerable:!0,get:function(){return c.GetStoredFilters}});var p=o(n(8822));t.LoadingScreen=p.default;var l=o(n(44811));t.LoadingIcon=l.default;var s=n(90782);Object.defineProperty(t,"ToolTip",{enumerable:!0,get:function(){return s.ToolTip}});var u=o(n(49272));t.TabSelector=u.default;var b=o(n(35620));t.ServerErrorIcon=b.default;var M=o(n(82589));t.GenericSlice=M.default;var d=o(n(60432));t.Application=d.default;var z=o(n(19320));t.Page=z.default;var O=o(n(49012));t.Section=O.default;var f=o(n(75754));t.SplitSection=f.default;var h=o(n(94131));t.VerticalSplit=h.default;var A=o(n(22332));t.SplitDrawer=A.default;var m=o(n(14044));t.OverlayDrawer=m.default;var q=o(n(48158));t.ProgressBar=q.default;var v=o(n(39545));t.BtnDropdown=v.default;var _=o(n(12447));t.GenericController=_.default;var y=o(n(50418));t.LayoutGrid=y.default;var g=o(n(83904));t.Alert=g.default;var W=o(n(11997));t.Breadcrumb=W.default;var L=n(95357);Object.defineProperty(t,"Gemstone",{enumerable:!0,get:function(){return L.Gemstone}});var R=o(n(49096));t.StatusProgressBar=R.default;var w=o(n(84579));t.ExternalPage=w.default;var S=o(n(9089));t.DecisionHelpTree=S.default;var x=o(n(8036));t.Map=x.default},85695:(e,t,n)=>{"use strict";n.r(t),n.d(t,{BrowserRouter:()=>ue,HashRouter:()=>be,Link:()=>de,MemoryRouter:()=>m,NavLink:()=>ze,Navigate:()=>q,Outlet:()=>v,Route:()=>_,Router:()=>y,Routes:()=>g,UNSAFE_LocationContext:()=>h,UNSAFE_NavigationContext:()=>f,UNSAFE_RouteContext:()=>A,createRoutesFromChildren:()=>P,createSearchParams:()=>he,generatePath:()=>C,matchPath:()=>$,matchRoutes:()=>j,renderMatches:()=>J,resolvePath:()=>ee,unstable_HistoryRouter:()=>Me,useHref:()=>W,useInRouterContext:()=>L,useLinkClickHandler:()=>Oe,useLocation:()=>R,useMatch:()=>S,useNavigate:()=>x,useNavigationType:()=>w,useOutlet:()=>N,useOutletContext:()=>T,useParams:()=>E,useResolvedPath:()=>B,useRoutes:()=>D,useSearchParams:()=>fe});var o,r=n(8674),a=n(89575);n(4364),function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"}(o||(o={}));var i=function(e){return e},c="beforeunload",p="hashchange",l="popstate";function s(e,t,n){return Math.min(Math.max(e,t),n)}function u(e){e.preventDefault(),e.returnValue=""}function b(){var e=[];return{get length(){return e.length},push:function(t){return e.push(t),function(){e=e.filter(function(e){return e!==t})}},call:function(t){e.forEach(function(e){return e&&e(t)})}}}function M(){return Math.random().toString(36).substr(2,8)}function d(e){var t=e.pathname,n=void 0===t?"/":t,o=e.search,r=void 0===o?"":o,a=e.hash,i=void 0===a?"":a;return r&&"?"!==r&&(n+="?"===r.charAt(0)?r:"?"+r),i&&"#"!==i&&(n+="#"===i.charAt(0)?i:"#"+i),n}function z(e){var t={};if(e){var n=e.indexOf("#");n>=0&&(t.hash=e.substr(n),e=e.substr(0,n));var o=e.indexOf("?");o>=0&&(t.search=e.substr(o),e=e.substr(0,o)),e&&(t.pathname=e)}return t}function O(e,t){if(!e)throw new Error(t)}n(4364);const f=(0,r.createContext)(null),h=(0,r.createContext)(null),A=(0,r.createContext)({outlet:null,matches:[]});function m(e){let{basename:t,children:n,initialEntries:c,initialIndex:p}=e,l=(0,r.useRef)();null==l.current&&(l.current=function(e){void 0===e&&(e={});var t=e,n=t.initialEntries,r=void 0===n?["/"]:n,c=t.initialIndex,p=r.map(function(e){return i((0,a.A)({pathname:"/",search:"",hash:"",state:null,key:M()},"string"==typeof e?z(e):e))}),l=s(null==c?p.length-1:c,0,p.length-1),u=o.Pop,O=p[l],f=b(),h=b();function A(e,t){return void 0===t&&(t=null),i((0,a.A)({pathname:O.pathname,search:"",hash:""},"string"==typeof e?z(e):e,{state:t,key:M()}))}function m(e,t,n){return!h.length||(h.call({action:e,location:t,retry:n}),!1)}function q(e,t){u=e,O=t,f.call({action:u,location:O})}function v(e){var t=s(l+e,0,p.length-1),n=o.Pop,r=p[t];m(n,r,function(){v(e)})&&(l=t,q(n,r))}var _={get index(){return l},get action(){return u},get location(){return O},createHref:function(e){return"string"==typeof e?e:d(e)},push:function e(t,n){var r=o.Push,a=A(t,n);m(r,a,function(){e(t,n)})&&(l+=1,p.splice(l,p.length,a),q(r,a))},replace:function e(t,n){var r=o.Replace,a=A(t,n);m(r,a,function(){e(t,n)})&&(p[l]=a,q(r,a))},go:v,back:function(){v(-1)},forward:function(){v(1)},listen:function(e){return f.push(e)},block:function(e){return h.push(e)}};return _}({initialEntries:c,initialIndex:p}));let u=l.current,[O,f]=(0,r.useState)({action:u.action,location:u.location});return(0,r.useLayoutEffect)(()=>u.listen(f),[u]),(0,r.createElement)(y,{basename:t,children:n,location:O.location,navigationType:O.action,navigator:u})}function q(e){let{to:t,replace:n,state:o}=e;L()||O(!1);let a=x();return(0,r.useEffect)(()=>{a(t,{replace:n,state:o})}),null}function v(e){return N(e.context)}function _(e){O(!1)}function y(e){let{basename:t="/",children:n=null,location:a,navigationType:i=o.Pop,navigator:c,static:p=!1}=e;L()&&O(!1);let l=re(t),s=(0,r.useMemo)(()=>({basename:l,navigator:c,static:p}),[l,c,p]);"string"==typeof a&&(a=z(a));let{pathname:u="/",search:b="",hash:M="",state:d=null,key:A="default"}=a,m=(0,r.useMemo)(()=>{let e=ne(u,l);return null==e?null:{pathname:e,search:b,hash:M,state:d,key:A}},[l,u,b,M,d,A]);return null==m?null:(0,r.createElement)(f.Provider,{value:s},(0,r.createElement)(h.Provider,{children:n,value:{location:m,navigationType:i}}))}function g(e){let{children:t,location:n}=e;return D(P(t),n)}function W(e){L()||O(!1);let{basename:t,navigator:n}=(0,r.useContext)(f),{hash:o,pathname:a,search:i}=B(e),c=a;if("/"!==t){let n=function(e){return""===e||""===e.pathname?"/":"string"==typeof e?z(e).pathname:e.pathname}(e),o=null!=n&&n.endsWith("/");c="/"===a?t+(o?"/":""):oe([t,a])}return n.createHref({pathname:c,search:i,hash:o})}function L(){return null!=(0,r.useContext)(h)}function R(){return L()||O(!1),(0,r.useContext)(h).location}function w(){return(0,r.useContext)(h).navigationType}function S(e){L()||O(!1);let{pathname:t}=R();return(0,r.useMemo)(()=>$(e,t),[t,e])}function x(){L()||O(!1);let{basename:e,navigator:t}=(0,r.useContext)(f),{matches:n}=(0,r.useContext)(A),{pathname:o}=R(),a=JSON.stringify(n.map(e=>e.pathnameBase)),i=(0,r.useRef)(!1);return(0,r.useEffect)(()=>{i.current=!0}),(0,r.useCallback)(function(n,r){if(void 0===r&&(r={}),!i.current)return;if("number"==typeof n)return void t.go(n);let c=te(n,JSON.parse(a),o);"/"!==e&&(c.pathname=oe([e,c.pathname])),(r.replace?t.replace:t.push)(c,r.state)},[e,t,a,o])}const k=(0,r.createContext)(null);function T(){return(0,r.useContext)(k)}function N(e){let t=(0,r.useContext)(A).outlet;return t?(0,r.createElement)(k.Provider,{value:e},t):t}function E(){let{matches:e}=(0,r.useContext)(A),t=e[e.length-1];return t?t.params:{}}function B(e){let{matches:t}=(0,r.useContext)(A),{pathname:n}=R(),o=JSON.stringify(t.map(e=>e.pathnameBase));return(0,r.useMemo)(()=>te(e,JSON.parse(o),n),[e,o,n])}function D(e,t){L()||O(!1);let{matches:n}=(0,r.useContext)(A),o=n[n.length-1],a=o?o.params:{},i=(o&&o.pathname,o?o.pathnameBase:"/");o&&o.route;let c,p=R();if(t){var l;let e="string"==typeof t?z(t):t;"/"===i||(null==(l=e.pathname)?void 0:l.startsWith(i))||O(!1),c=e}else c=p;let s=c.pathname||"/",u=j(e,{pathname:"/"===i?s:s.slice(i.length)||"/"});return Q(u&&u.map(e=>Object.assign({},e,{params:Object.assign({},a,e.params),pathname:oe([i,e.pathname]),pathnameBase:"/"===e.pathnameBase?i:oe([i,e.pathnameBase])})),n)}function P(e){let t=[];return r.Children.forEach(e,e=>{if(!(0,r.isValidElement)(e))return;if(e.type===r.Fragment)return void t.push.apply(t,P(e.props.children));e.type!==_&&O(!1);let n={caseSensitive:e.props.caseSensitive,element:e.props.element,index:e.props.index,path:e.props.path};e.props.children&&(n.children=P(e.props.children)),t.push(n)}),t}function C(e,t){return void 0===t&&(t={}),e.replace(/:(\w+)/g,(e,n)=>(null==t[n]&&O(!1),t[n])).replace(/\/*\*$/,e=>null==t["*"]?"":t["*"].replace(/^\/*/,"/"))}function j(e,t,n){void 0===n&&(n="/");let o=ne(("string"==typeof t?z(t):t).pathname||"/",n);if(null==o)return null;let r=Y(e);!function(e){e.sort((e,t)=>e.score!==t.score?t.score-e.score:function(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)))}(r);let a=null;for(let e=0;null==a&&e{let a={relativePath:e.path||"",caseSensitive:!0===e.caseSensitive,childrenIndex:r,route:e};a.relativePath.startsWith("/")&&(a.relativePath.startsWith(o)||O(!1),a.relativePath=a.relativePath.slice(o.length));let i=oe([o,a.relativePath]),c=n.concat(a);e.children&&e.children.length>0&&(!0===e.index&&O(!1),Y(e.children,t,c,i)),(null!=e.path||e.index)&&t.push({path:i,score:K(i,e.index),routesMeta:c})}),t}const X=/^:\w+$/,F=3,H=2,I=1,U=10,G=-2,V=e=>"*"===e;function K(e,t){let n=e.split("/"),o=n.length;return n.some(V)&&(o+=G),t&&(o+=H),n.filter(e=>!V(e)).reduce((e,t)=>e+(X.test(t)?F:""===t?I:U),o)}function Z(e,t){let{routesMeta:n}=e,o={},r="/",a=[];for(let e=0;e(0,r.createElement)(A.Provider,{children:void 0!==o.route.element?o.route.element:(0,r.createElement)(v,null),value:{outlet:n,matches:t.concat(e.slice(0,a+1))}}),null)}function $(e,t){"string"==typeof e&&(e={path:e,caseSensitive:!1,end:!0});let[n,o]=function(e,t,n){void 0===t&&(t=!1),void 0===n&&(n=!0);let o=[],r="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^$?{}|()[\]]/g,"\\$&").replace(/:(\w+)/g,(e,t)=>(o.push(t),"([^\\/]+)"));return e.endsWith("*")?(o.push("*"),r+="*"===e||"/*"===e?"(.*)$":"(?:\\/(.+)|\\/*)$"):r+=n?"\\/*$":"(?:\\b|\\/|$)",[new RegExp(r,t?void 0:"i"),o]}(e.path,e.caseSensitive,e.end),r=t.match(n);if(!r)return null;let a=r[0],i=a.replace(/(.)\/+$/,"$1"),c=r.slice(1);return{params:o.reduce((e,t,n)=>{if("*"===t){let e=c[n]||"";i=a.slice(0,a.length-e.length).replace(/(.)\/+$/,"$1")}return e[t]=function(e){try{return decodeURIComponent(e)}catch(t){return e}}(c[n]||""),e},{}),pathname:a,pathnameBase:i,pattern:e}}function ee(e,t){void 0===t&&(t="/");let{pathname:n,search:o="",hash:r=""}="string"==typeof e?z(e):e,a=n?n.startsWith("/")?n:function(e,t){let n=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(e=>{".."===e?n.length>1&&n.pop():"."!==e&&n.push(e)}),n.length>1?n.join("/"):"/"}(n,t):t;return{pathname:a,search:ae(o),hash:ie(r)}}function te(e,t,n){let o,r="string"==typeof e?z(e):e,a=""===e||""===r.pathname?"/":r.pathname;if(null==a)o=n;else{let e=t.length-1;if(a.startsWith("..")){let t=a.split("/");for(;".."===t[0];)t.shift(),e-=1;r.pathname=t.join("/")}o=e>=0?t[e]:"/"}let i=ee(r,o);return a&&"/"!==a&&a.endsWith("/")&&!i.pathname.endsWith("/")&&(i.pathname+="/"),i}function ne(e,t){if("/"===t)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=e.charAt(t.length);return n&&"/"!==n?null:e.slice(t.length)||"/"}const oe=e=>e.join("/").replace(/\/\/+/g,"/"),re=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),ae=e=>e&&"?"!==e?e.startsWith("?")?e:"?"+e:"",ie=e=>e&&"#"!==e?e.startsWith("#")?e:"#"+e:"";function ce(){return ce=Object.assign||function(e){for(var t=1;t=0||(r[n]=e[n]);return r}n(4364);const le=["onClick","reloadDocument","replace","state","target","to"],se=["aria-current","caseSensitive","className","end","style","to","children"];function ue(e){let{basename:t,children:n,window:p}=e,s=(0,r.useRef)();null==s.current&&(s.current=function(e){void 0===e&&(e={});var t=e.window,n=void 0===t?document.defaultView:t,r=n.history;function p(){var e=n.location,t=e.pathname,o=e.search,a=e.hash,c=r.state||{};return[c.idx,i({pathname:t,search:o,hash:a,state:c.usr||null,key:c.key||"default"})]}var s=null;n.addEventListener(l,function(){if(s)q.call(s),s=null;else{var e=o.Pop,t=p(),n=t[0],r=t[1];if(q.length){if(null!=n){var a=h-n;a&&(s={action:e,location:r,retry:function(){L(-1*a)}},L(a))}}else W(e)}});var O=o.Pop,f=p(),h=f[0],A=f[1],m=b(),q=b();function v(e){return"string"==typeof e?e:d(e)}function _(e,t){return void 0===t&&(t=null),i((0,a.A)({pathname:A.pathname,hash:"",search:""},"string"==typeof e?z(e):e,{state:t,key:M()}))}function y(e,t){return[{usr:e.state,key:e.key,idx:t},v(e)]}function g(e,t,n){return!q.length||(q.call({action:e,location:t,retry:n}),!1)}function W(e){O=e;var t=p();h=t[0],A=t[1],m.call({action:O,location:A})}function L(e){r.go(e)}null==h&&(h=0,r.replaceState((0,a.A)({},r.state,{idx:h}),""));var R={get action(){return O},get location(){return A},createHref:v,push:function e(t,a){var i=o.Push,c=_(t,a);if(g(i,c,function(){e(t,a)})){var p=y(c,h+1),l=p[0],s=p[1];try{r.pushState(l,"",s)}catch(e){n.location.assign(s)}W(i)}},replace:function e(t,n){var a=o.Replace,i=_(t,n);if(g(a,i,function(){e(t,n)})){var c=y(i,h),p=c[0],l=c[1];r.replaceState(p,"",l),W(a)}},go:L,back:function(){L(-1)},forward:function(){L(1)},listen:function(e){return m.push(e)},block:function(e){var t=q.push(e);return 1===q.length&&n.addEventListener(c,u),function(){t(),q.length||n.removeEventListener(c,u)}}};return R}({window:p}));let O=s.current,[f,h]=(0,r.useState)({action:O.action,location:O.location});return(0,r.useLayoutEffect)(()=>O.listen(h),[O]),(0,r.createElement)(y,{basename:t,children:n,location:f.location,navigationType:f.action,navigator:O})}function be(e){let{basename:t,children:n,window:s}=e,O=(0,r.useRef)();null==O.current&&(O.current=function(e){void 0===e&&(e={});var t=e.window,n=void 0===t?document.defaultView:t,r=n.history;function s(){var e=z(n.location.hash.substr(1)),t=e.pathname,o=void 0===t?"/":t,a=e.search,c=void 0===a?"":a,p=e.hash,l=void 0===p?"":p,s=r.state||{};return[s.idx,i({pathname:o,search:c,hash:l,state:s.usr||null,key:s.key||"default"})]}var O=null;function f(){if(O)_.call(O),O=null;else{var e=o.Pop,t=s(),n=t[0],r=t[1];if(_.length){if(null!=n){var a=m-n;a&&(O={action:e,location:r,retry:function(){w(-1*a)}},w(a))}}else R(e)}}n.addEventListener(l,f),n.addEventListener(p,function(){d(s()[1])!==d(q)&&f()});var h=o.Pop,A=s(),m=A[0],q=A[1],v=b(),_=b();function y(e){return function(){var e=document.querySelector("base"),t="";if(e&&e.getAttribute("href")){var o=n.location.href,r=o.indexOf("#");t=-1===r?o:o.slice(0,r)}return t}()+"#"+("string"==typeof e?e:d(e))}function g(e,t){return void 0===t&&(t=null),i((0,a.A)({pathname:q.pathname,hash:"",search:""},"string"==typeof e?z(e):e,{state:t,key:M()}))}function W(e,t){return[{usr:e.state,key:e.key,idx:t},y(e)]}function L(e,t,n){return!_.length||(_.call({action:e,location:t,retry:n}),!1)}function R(e){h=e;var t=s();m=t[0],q=t[1],v.call({action:h,location:q})}function w(e){r.go(e)}return null==m&&(m=0,r.replaceState((0,a.A)({},r.state,{idx:m}),"")),{get action(){return h},get location(){return q},createHref:y,push:function e(t,a){var i=o.Push,c=g(t,a);if(L(i,c,function(){e(t,a)})){var p=W(c,m+1),l=p[0],s=p[1];try{r.pushState(l,"",s)}catch(e){n.location.assign(s)}R(i)}},replace:function e(t,n){var a=o.Replace,i=g(t,n);if(L(a,i,function(){e(t,n)})){var c=W(i,m),p=c[0],l=c[1];r.replaceState(p,"",l),R(a)}},go:w,back:function(){w(-1)},forward:function(){w(1)},listen:function(e){return v.push(e)},block:function(e){var t=_.push(e);return 1===_.length&&n.addEventListener(c,u),function(){t(),_.length||n.removeEventListener(c,u)}}}}({window:s}));let f=O.current,[h,A]=(0,r.useState)({action:f.action,location:f.location});return(0,r.useLayoutEffect)(()=>f.listen(A),[f]),(0,r.createElement)(y,{basename:t,children:n,location:h.location,navigationType:h.action,navigator:f})}function Me(e){let{basename:t,children:n,history:o}=e;const[a,i]=(0,r.useState)({action:o.action,location:o.location});return(0,r.useLayoutEffect)(()=>o.listen(i),[o]),(0,r.createElement)(y,{basename:t,children:n,location:a.location,navigationType:a.action,navigator:o})}const de=(0,r.forwardRef)(function(e,t){let{onClick:n,reloadDocument:o,replace:a=!1,state:i,target:c,to:p}=e,l=pe(e,le),s=W(p),u=Oe(p,{replace:a,state:i,target:c});return(0,r.createElement)("a",ce({},l,{href:s,onClick:function(e){n&&n(e),e.defaultPrevented||o||u(e)},ref:t,target:c}))}),ze=(0,r.forwardRef)(function(e,t){let{"aria-current":n="page",caseSensitive:o=!1,className:a="",end:i=!1,style:c,to:p,children:l}=e,s=pe(e,se),u=R(),b=B(p),M=u.pathname,d=b.pathname;o||(M=M.toLowerCase(),d=d.toLowerCase());let z,O=M===d||!i&&M.startsWith(d)&&"/"===M.charAt(d.length),f=O?n:void 0;z="function"==typeof a?a({isActive:O}):[a,O?"active":null].filter(Boolean).join(" ");let h="function"==typeof c?c({isActive:O}):c;return(0,r.createElement)(de,ce({},s,{"aria-current":f,className:z,ref:t,style:h,to:p}),"function"==typeof l?l({isActive:O}):l)});function Oe(e,t){let{target:n,replace:o,state:a}=void 0===t?{}:t,i=x(),c=R(),p=B(e);return(0,r.useCallback)(t=>{if(!(0!==t.button||n&&"_self"!==n||function(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}(t))){t.preventDefault();let n=!!o||d(c)===d(p);i(e,{replace:n,state:a})}},[c,i,p,o,a,n,e])}function fe(e){let t=(0,r.useRef)(he(e)),n=R(),o=(0,r.useMemo)(()=>{let e=he(n.search);for(let n of t.current.keys())e.has(n)||t.current.getAll(n).forEach(t=>{e.append(n,t)});return e},[n.search]),a=x();return[o,(0,r.useCallback)((e,t)=>{a("?"+he(e),t)},[a])]}function he(e){return void 0===e&&(e=""),new URLSearchParams("string"==typeof e||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,n)=>{let o=e[n];return t.concat(Array.isArray(o)?o.map(e=>[n,e]):[[n,o]])},[]))}},49815:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return o.createElement(o.Fragment,null,e.children)};const o=n(8674)},81201:(e,t,n)=>{"use strict";var o=n(4364);Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){const t=()=>{var t,n,o;const a=new Map,i=null!==(o=null===(n=localStorage.getItem(null!==(t=e.LocalStorageKey)&&void 0!==t?t:""))||void 0===n?void 0:n.split(","))&&void 0!==o?o:[];return""==i[0]&&i.pop(),r.Children.forEach(e.children,e=>{var t,n;if(r.isValidElement(e)&&e.type===b.default){const o=e.props.Key,r={Key:o,Label:null!==(t=e.props.Label)&&void 0!==t?t:o,Default:null!==(n=e.props.Default)&&void 0!==n&&n,Enabled:!1};r.Enabled=i.length>0?i.includes(o):y(r),a.set(o,r)}}),a},[n,d]=r.useState(!1),[z,O]=r.useState(()=>t()),[f,h]=r.useState(!1),[A]=r.useState((0,l.CreateGuid)()),[m,q]=r.useState(!1),v=r.useCallback(e=>{0!==e.length?q(!0):q(!1)},[]);function _(e){O(t=>{var n,r;const a=u.cloneDeep(t),i=a.get(e);return null==i?o.error("Could not find reference for column "+e):i.Enabled=!(null!==(r=null===(n=a.get(e))||void 0===n?void 0:n.Enabled)&&void 0!==r&&r),a})}function y(t,n=!0){if(null==t)return!1;const o=!0===t.Default,r=e.SortKey===t.Key,a=n&&function(t){if(void 0===e.LocalStorageKey)return!1;const n=localStorage.getItem(e.LocalStorageKey);return null!==n&&n.split(",").includes(null!=t?t:"")}(t.Key);return r||a||o}return r.useEffect(()=>{O(()=>t())},[e.children]),r.useEffect(()=>{void 0!==e.OnSettingsChange&&e.OnSettingsChange(n)},[n]),r.useEffect(()=>{!function(){if(void 0===e.LocalStorageKey)return;const t=localStorage.getItem(e.LocalStorageKey),n=Array.from(z.keys());let o=[];null!==t&&(o=t.split(",")),o=o.filter(e=>!n.includes(e));const r=n.filter(e=>{var t;return null===(t=z.get(e))||void 0===t?void 0:t.Enabled});o.push(...r),localStorage.setItem(e.LocalStorageKey,o.join(","))}()},[z]),r.createElement(r.Fragment,null,r.createElement(a.Table,Object.assign({},e,{LastColumn:r.createElement("div",{style:{marginLeft:-5,marginBottom:12,cursor:"pointer"},onMouseEnter:()=>h(!0),onMouseLeave:()=>h(!1),id:A+"-tooltip",onClick:()=>d(!0)},r.createElement(i.ReactIcons.Settings,null)),ReduceWidthCallback:v}),r.Children.map(e.children,e=>{var t,n;return r.isValidElement(e)?e.type===b.default?null!==(n=null===(t=z.get(e.props.Key))||void 0===t?void 0:t.Enabled)&&void 0!==n&&n?e.props.children:null:e:null})),r.createElement(s.ToolTip,{Show:f,Position:"bottom",Target:A+"-tooltip",Zindex:99999},r.createElement("p",null,"Change Columns")),void 0===e.SettingsPortal?r.createElement(c.Modal,{Title:"Table Columns",Show:n,ShowX:!0,ShowCancel:!1,ZIndex:e.ModalZIndex,CallBack:e=>{d(!1),e&&O(e=>{const t=u.cloneDeep(e);return Array.from(e.keys()).forEach(e=>{var n;const o=t.get(e);null!=o&&(o.Enabled=null===(n=y(t.get(e),!1))||void 0===n||n)}),t})},ConfirmText:"Reset Defaults",ConfirmBtnClass:"btn-warning float-left"},r.createElement(M,{columns:Array.from(z.values()),onChange:_,sortKey:e.SortKey,disableAdd:m})):n?r.createElement(p.Portal,{node:null===document||void 0===document?void 0:document.getElementById(e.SettingsPortal)},r.createElement("div",{className:"card"},r.createElement("div",{className:"card-header"},r.createElement("h4",{className:"modal-title"},"Table Columns"),r.createElement("button",{type:"button",className:"close",onClick:()=>d(!1)},"×")),r.createElement("div",{className:"card-body",style:{maxHeight:"calc(100% - 210px)",overflowY:"auto"}},r.createElement(M,{columns:Array.from(z.values()),onChange:_,sortKey:e.SortKey,disableAdd:m})),r.createElement("div",{className:"card-footer"},r.createElement("button",{type:"button",className:"btn btn-primary float-left",onClick:()=>{d(!1),O(e=>{const t=u.cloneDeep(e);return Array.from(e.keys()).forEach(e=>{var n;const o=t.get(e);null!=o&&(o.Enabled=null===(n=y(t.get(e),!0))||void 0===n||n)}),t})}},"Reset Defaults")))):null)};const r=n(8674),a=n(74797),i=n(36400),c=n(68976),p=n(3105),l=n(76752),s=n(90782),u=n(42005),b=n(49815);function M(e){const t=1===e.columns.filter(e=>e.Enabled).length,n=n=>n.Key===e.sortKey?"The Table is currently sorted by this column, so it cannot be hidden.":t&&n.Enabled?"The Table must have one column visible at all times, so it cannot be hidden.":void 0;return r.createElement(r.Fragment,null,r.createElement("div",{className:"row"},r.createElement("div",{className:"col-4"},e.columns.map((o,a)=>a%3==0?r.createElement(s.CheckBox,{Label:o.Label,Field:"Enabled",Record:o,Setter:()=>e.onChange(o.Key),key:o.Key,Disabled:o.Key===e.sortKey||e.disableAdd&&!o.Enabled||t&&o.Enabled,Help:n(o)}):null)),r.createElement("div",{className:"col-4"},e.columns.map((o,a)=>a%3==1?r.createElement(s.CheckBox,{Label:o.Label,Field:"Enabled",Record:o,Setter:()=>e.onChange(o.Key),key:o.Key,Disabled:o.Key===e.sortKey||e.disableAdd&&!o.Enabled||t&&o.Enabled,Help:n(o)}):null)),r.createElement("div",{className:"col-4"},e.columns.map((o,a)=>a%3==2?r.createElement(s.CheckBox,{Label:o.Label,Field:"Enabled",Record:o,Setter:()=>e.onChange(o.Key),key:o.Key,Disabled:o.Key===e.sortKey||e.disableAdd&&!o.Enabled||t&&o.Enabled,Help:n(o)}):null))),e.disableAdd?r.createElement(c.Alert,{Class:"alert-primary",Style:{marginBottom:0,marginTop:"0.5em"}},"Additional columns disabled due to table size."):null)}},42780:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.BooleanFilter=function(e){const[t,n]=o.useState(!1),[r,a]=o.useState(!1);return o.useEffect(()=>{if(0===e.Filter.length)return n(!0),void a(!0);n("1"===e.Filter[0].SearchText),a("1"!==e.Filter[0].SearchText)},[e.Filter]),o.useEffect(()=>{t||r||(n(!0),a(!0))},[t,r]),o.useEffect(()=>{!t||r||0!==e.Filter.length&&"1"===e.Filter[0].SearchText||e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,SearchText:"1",Operator:"=",Type:"boolean"}]),t||!r||0!==e.Filter.length&&"0"===e.Filter[0].SearchText||e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,SearchText:"0",Operator:"=",Type:"boolean"}]),t&&r&&e.Filter.length>0&&e.SetFilter([])},[t,r]),o.createElement(o.Fragment,null,o.createElement("tr",{onClick:e=>{e.preventDefault(),n(e=>!e)}},o.createElement("td",null,o.createElement("input",{type:"checkbox",checked:t,onChange:()=>null})),o.createElement("td",null,"Selected")),o.createElement("tr",{onClick:e=>{e.preventDefault(),a(e=>!e)}},o.createElement("td",null,o.createElement("input",{type:"checkbox",checked:r,onChange:()=>null})),o.createElement("td",null,"Not Selected")))};const o=n(8674)},56310:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.DateFilter=function(e){const[t,n]=o.useState(""),[r,a]=o.useState(""),[i,c]=o.useState("after");return o.useEffect(()=>{if(0===e.Filter.length&&(n(""),a("")),e.Filter.length>1){const t=e.Filter.find(e=>">"===e.Operator||">="===e.Operator);n(null==t?"":t.SearchText);const o=e.Filter.find(e=>"<"===e.Operator||"<="===e.Operator);a(null==o?"":o.SearchText),c(null!==t&&null!==o?"between":null==t?"before":"after")}1===e.Filter.length&&(a(""),">"===e.Filter[0].Operator||">="===e.Filter[0].Operator?c("after"):c("before"),n(e.Filter[0].SearchText))},[e.Filter]),o.useEffect(()=>{let n=null;return""===t&&""===r&&0!==e.Filter.length&&(n=setTimeout(()=>e.SetFilter([]),500)),""===t&&""===r||(n="between"===i?setTimeout(()=>e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,Operator:">=",Type:"datetime",SearchText:t},{FieldName:e.FieldName,IsPivotColumn:!1,Operator:"<=",Type:"datetime",SearchText:r}]),500):setTimeout(()=>e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,Operator:"after"===i?">":"<",Type:"datetime",SearchText:t}]),500)),()=>{null!==n&&clearTimeout(n)}},[i,t,r]),o.createElement(o.Fragment,null,o.createElement("tr",null,o.createElement("td",null,o.createElement("select",{className:"form-control",value:i,onChange:e=>{const t=e.target.value;c(t)}},o.createElement("option",{value:"before"},"Before"),o.createElement("option",{value:"after"},"After"),o.createElement("option",{value:"between"},"Between")))),o.createElement("tr",null,o.createElement("td",null,o.createElement("input",{type:"date",className:"form-control",value:t,onChange:e=>{const t=e.target.value;n(t)}}))),"between"===i?o.createElement(o.Fragment,null,o.createElement("tr",null,o.createElement("td",null,"and")),o.createElement("tr",null,o.createElement("td",null,o.createElement("input",{type:"date",className:"form-control",value:r,onChange:e=>{const t=e.target.value;a(t)}})))):null)},t.TimeFilter=function(e){const[t,n]=o.useState(""),[r,a]=o.useState(""),[i,c]=o.useState("after");return o.useEffect(()=>{if(0===e.Filter.length&&(n(""),a("")),e.Filter.length>1){const t=e.Filter.find(e=>">"===e.Operator||">="===e.Operator);n(null==t?"":t.SearchText);const o=e.Filter.find(e=>"<"===e.Operator||"<="===e.Operator);a(null==o?"":o.SearchText),c(null!==t&&null!==o?"between":null==t?"before":"after")}1===e.Filter.length&&(a(""),">"===e.Filter[0].Operator||">="===e.Filter[0].Operator?c("after"):c("before"),n(e.Filter[0].SearchText))},[e.Filter]),o.useEffect(()=>{let n=null;return""===t&&""===r&&0!==e.Filter.length&&(n=setTimeout(()=>e.SetFilter([]),500)),""===t&&""===r||(n="between"===i?setTimeout(()=>e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,Operator:">=",Type:"datetime",SearchText:t},{FieldName:e.FieldName,IsPivotColumn:!1,Operator:"<=",Type:"datetime",SearchText:r}]),500):setTimeout(()=>e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,Operator:"after"===i?">":"<",Type:"datetime",SearchText:t}]),500)),()=>{null!==n&&clearTimeout(n)}},[i,t,r]),o.createElement(o.Fragment,null,o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,o.createElement("select",{className:"form-control",value:i,onChange:e=>{const t=e.target.value;c(t)}},o.createElement("option",{value:"before"},"Before"),o.createElement("option",{value:"after"},"After"),o.createElement("option",{value:"between"},"Between")))),o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,o.createElement("input",{type:"time",className:"form-control",value:t,onChange:e=>{const t=e.target.value;n(t)}}))),"between"===i?o.createElement(o.Fragment,null,o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,"and")),o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,o.createElement("input",{type:"time",className:"form-control",value:r,onChange:e=>{const t=e.target.value;a(t)}})))):null)},t.DateTimeFilter=function(e){const[t,n]=o.useState(""),[c,p]=o.useState(""),l=o.useMemo(()=>({Value:t}),[t]),s=o.useMemo(()=>({Value:c}),[c]),[u,b]=o.useState("after");return o.useEffect(()=>{if(0===e.Filter.length&&(n(""),p("")),e.Filter.length>1){const t=e.Filter.find(e=>">"===e.Operator||">="===e.Operator);n(null==t?"":t.SearchText);const o=e.Filter.find(e=>"<"===e.Operator||"<="===e.Operator);p(null==o?"":o.SearchText),b(null!==t&&null!==o?"between":null==t?"before":"after")}1===e.Filter.length&&(p(""),">"===e.Filter[0].Operator||">="===e.Filter[0].Operator?b("after"):b("before"),n(e.Filter[0].SearchText))},[e.Filter]),o.useEffect(()=>{let n=null;return""===t&&""===c&&0!==e.Filter.length&&(n=setTimeout(()=>e.SetFilter([]),500)),""===t&&""===c||(n="between"===u?setTimeout(()=>e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,Operator:">=",Type:"datetime",SearchText:t},{FieldName:e.FieldName,IsPivotColumn:!1,Operator:"<=",Type:"datetime",SearchText:c}]),500):setTimeout(()=>e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,Operator:"after"===u?">":"<",Type:"datetime",SearchText:t}]),500)),()=>{null!==n&&clearTimeout(n)}},[u,t,c]),o.createElement(o.Fragment,null,o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,o.createElement("select",{className:"form-control",value:u,onChange:e=>{const t=e.target.value;b(t)}},o.createElement("option",{value:"before"},"Before"),o.createElement("option",{value:"after"},"After"),o.createElement("option",{value:"between"},"Between")))),o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,o.createElement(r.DatePicker,{Record:l,Field:"Value",Setter:e=>n(e.Value),Label:"",Type:"datetime-local",Valid:()=>!0,Format:a+" "+i}))),"between"===u?o.createElement(o.Fragment,null,o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,"and")),o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,o.createElement(r.DatePicker,{Record:s,Field:"Value",Setter:e=>p(e.Value),Label:"",Type:"datetime-local",Valid:()=>!0,Format:a+" "+i})))):null)};const o=n(8674),r=n(90782),a="MM/DD/YYYY",i="HH:mm:ss.SSS"},52319:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.EnumFilter=function(e){const[t,n]=o.useState([]);return o.useEffect(()=>{(e.Options.length!==t.length||e.Options.some((e,n)=>e.Label!==t[n].Label||e.Value!==t[n].Value))&&n(e.Options.map(e=>Object.assign(Object.assign({},e),{Selected:!0})))},[e.Options]),o.useEffect(()=>{0===e.Filter.length||t.filter(e=>e.Selected).length!==t.length?t.some(e=>!e.Selected)&&e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,Operator:"IN",Type:"enum",SearchText:`(${t.filter(e=>e.Selected).map(e=>e.Value).join(",")})`}]):e.SetFilter([])},[t]),o.useEffect(()=>{if(0===e.Filter.length)n(e=>e.map(e=>Object.assign(Object.assign({},e),{Selected:!0})));else{let o=e.Filter[0].SearchText.replace("(","").replace(")","").split(",");o=o.filter(e=>""!==e),t.some(e=>{const t=o.findIndex(t=>t===e.Value);return!!(t<0&&e.Selected)||t>=0&&!e.Selected})&&n(e=>e.map(e=>Object.assign(Object.assign({},e),{Selected:o.findIndex(t=>t==e.Value)>=0})))}},[e.Filter]),o.createElement(o.Fragment,null,o.createElement("tr",{onClick:e=>{e.preventDefault();const o=t.filter(e=>e.Selected).length===t.length;n(e=>e.map(e=>Object.assign(Object.assign({},e),{Selected:!o})))}},o.createElement("td",null,o.createElement("input",{type:"checkbox",checked:t.filter(e=>e.Selected).length===t.length,onChange:()=>null})),o.createElement("td",null,"All")),t.map((e,t)=>o.createElement("tr",{key:t,onClick:()=>{n(t=>t.map(t=>Object.assign(Object.assign({},t),{Selected:t.Value===e.Value?!t.Selected:t.Selected})))}},o.createElement("td",null,o.createElement("input",{type:"checkbox",checked:e.Selected,onChange:()=>null})),o.createElement("td",null,e.Label))))};const o=n(8674)},2228:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.NumberFilter=function(e){var t,n,r,a,i;const[c,p]=o.useState(""),[l,s]=o.useState(""),[u,b]=o.useState("less than"),[M,d]=o.useState(0);o.useEffect(()=>{if(0===e.Filter.length&&(p(""),s("")),e.Filter.length>1){const t=e.Filter.find(e=>">"===e.Operator||">="===e.Operator);p(null==t?"":t.SearchText);const n=e.Filter.find(e=>"<"===e.Operator||"<="===e.Operator);s(null==n?"":n.SearchText)}1===e.Filter.length&&(s(""),">"===e.Filter[0].Operator||">="===e.Filter[0].Operator?b("greater than"):"="===e.Filter[0].Operator?b("equal to"):b("less than"),p(e.Filter[0].SearchText))},[e.Filter]),o.useEffect(()=>{let t=null;return""===c&&""===l&&0!==e.Filter.length&&(t=setTimeout(()=>e.SetFilter([]),500)),""===c&&""===l||(t="between"===u?setTimeout(()=>e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,Operator:">=",Type:"number",SearchText:c},{FieldName:e.FieldName,IsPivotColumn:!1,Operator:"<=",Type:"number",SearchText:l}]),500):setTimeout(()=>{return e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,Operator:(t=u,"less than"===t?"<":"greater than"===t?">":"="),Type:"number",SearchText:c}]);var t},500)),()=>{null!==t&&clearTimeout(t)}},[u,c,l]);const z=void 0!==e.Unit&&M>=0&&M{e.preventDefault()}},o.createElement("td",null,o.createElement("select",{className:"form-control",value:u,onChange:e=>{const t=e.target.value;b(t)}},o.createElement("option",{value:"less than"},"Less than (","<",")"),o.createElement("option",{value:"equal to"},"Equal to (=)"),o.createElement("option",{value:"greater than"},"Greater than (",">",")"),o.createElement("option",{value:"between"},"In range")))),o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,o.createElement("input",{type:"number",className:"form-control",value:void 0!==e.Unit&&z?null!==(t=e.Unit[M].GetValue(parseFloat(c)).toString())&&void 0!==t?t:"":c,onChange:t=>{let n=t.target.value;void 0!==e.Unit&&M>=0&&M{var n,o;const r=t.target.value;d(null!==(o=null===(n=e.Unit)||void 0===n?void 0:n.findIndex(e=>e.label===r))&&void 0!==o?o:-1)}},e.Unit.map(e=>o.createElement("option",{value:e.label,key:e.label},e.label)))):null),"between"===u?o.createElement(o.Fragment,null,o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,"and")),o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,o.createElement("input",{type:"number",className:"form-control",value:void 0!==e.Unit&&z?e.Unit[M].GetValue(parseFloat(l)).toString():l,onChange:t=>{let n=t.target.value;void 0!==e.Unit&&M>=0&&M{var n,o;const r=t.target.value;d(null!==(o=null===(n=e.Unit)||void 0===n?void 0:n.findIndex(e=>e.label===r))&&void 0!==o?o:-1)}},e.Unit.map(e=>o.createElement("option",{value:e.label,key:e.label},e.label)))):null):null)};const o=n(8674)},61253:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.TextFilter=function(e){const[t,n]=o.useState("");return o.useEffect(()=>{var t;0!==e.Filter.length?null===(t=e.ApproxMatches)||void 0===t||t?n(e.Filter[0].SearchText.substring(1,e.Filter[0].SearchText.length-1)):n(e.Filter[0].SearchText):n("")},[e.Filter]),o.useEffect(()=>{let n=null;return null!=t&&0!==t.trim().length||0===e.Filter.length||(n=setTimeout(()=>e.SetFilter([]),500)),null!=t&&t.trim().length>0&&(0===e.Filter.length||e.Filter[0].SearchText!==t.trim())&&(n=setTimeout(()=>e.SetFilter([{FieldName:e.FieldName,IsPivotColumn:!1,SearchText:void 0===e.ApproxMatches||e.ApproxMatches?"%"+t.trim()+"%":t.trim(),Operator:"LIKE",Type:"string"}]),500)),()=>{null!==n&&clearTimeout(n)}},[t]),o.createElement(o.Fragment,null,o.createElement("tr",{onClick:e=>{e.preventDefault()}},o.createElement("td",null,o.createElement("input",{className:"form-control",value:t.replace("$_","_"),placeholder:"Search",onChange:e=>{const t=e.target.value;n(t.replace("_","$_"))}}))),o.createElement("tr",null,o.createElement("td",null," ",o.createElement("label",null,"Wildcard (*) can be used")," ")))};const o=n(8674)},69928:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});const o=n(36400),r=n(76752),a=n(8674);t.default=e=>{const t=a.useRef(null),{scrollWidth:n,width:i}=(0,r.useGetContainerPosition)(t),[c,p]=a.useState([]),[l,s]=a.useState(7),u=a.useMemo(()=>{const e=Math.min(...c);return!isFinite(e)||isNaN(e)?0:e},[c]),b=a.useMemo(()=>{const e=Math.max(...c);return!isFinite(e)||isNaN(e)?0:e},[c]);a.useEffect(()=>{const t=[];let n=Math.max(e.Current-Math.floor(l/2),1);for(e.Total-n{n>i&&s(e=>Math.max(e-1,1))},[i,n]),a.useLayoutEffect(()=>{if(l>=7||null==t.current)return;const e=Array.from(t.current.children).reduce((e,t)=>e+t.offsetWidth,0);i-e>=50&&s(e=>Math.min(7,e+1))},[i,l]);const M=l<7?1!==u:u>2,d=l<7?b!==e.Total:b+2<=e.Total;return a.createElement("ul",{className:"pagination justify-content-center",ref:t},a.createElement("li",{className:"page-item"+(u<=1?" disabled":""),key:"previous",style:{cursor:"pointer"}},a.createElement("a",{className:"page-link",onClick:()=>{u>1&&e.SetPage(Math.max(e.Current-7,1))}},a.createElement(o.ReactIcons.DoubleChevronLeft,{Size:15}))),a.createElement("li",{className:"page-item"+(e.Current<=1?" disabled":""),key:"step-previous",style:{cursor:"pointer"}},a.createElement("a",{className:"page-link",onClick:()=>{e.Current>1&&e.SetPage(e.Current-1)}},a.createElement(o.ReactIcons.ChevronLeft,{Size:15}))),M?a.createElement(a.Fragment,null,a.createElement("li",{className:"page-item",key:"1",style:{cursor:"pointer"}},a.createElement("a",{className:"page-link",onClick:()=>e.SetPage(1)},"1")),a.createElement("li",{className:"page-item disabled",key:"skip-1"},a.createElement("a",{className:"page-link"},"..."))):null,c.map(t=>a.createElement("li",{className:"page-item"+(t==e.Current?" active":""),key:t,style:{cursor:"pointer"}},a.createElement("a",{className:"page-link",onClick:()=>e.SetPage(t)},t))),d?a.createElement(a.Fragment,null,a.createElement("li",{className:"page-item disabled",key:"skip-2"},a.createElement("a",{className:"page-link"},"...")),a.createElement("li",{className:"page-item",key:e.Total,style:{cursor:"pointer"}},a.createElement("a",{className:"page-link",onClick:()=>e.SetPage(e.Total)},e.Total))):null,a.createElement("li",{className:"page-item"+(e.Current>=e.Total?" disabled":""),key:"step-next",style:{cursor:"pointer"}},a.createElement("a",{className:"page-link",onClick:()=>{e.Current{b{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Column=function(e){return r.createElement(r.Fragment,null,e.children)},t.ColumnHeaderWrapper=function(e){var t;const[n,a]=r.useState(!1),i=r.useCallback(()=>{a(!0)},[]),c=r.useCallback(()=>{a(!1)},[]),p=r.useCallback(t=>{null!=e.startAdjustment&&e.startAdjustment(t)},[e.startAdjustment]),l=r.useCallback(t=>{var n;(null===(n=e.allowSort)||void 0===n||n)&&e.onSort(t)},[e.onSort,e.allowSort]),s=r.useCallback(e=>{e.stopPropagation()},[]);return r.createElement("th",{style:e.style,onClick:l,onDrag:e=>{e.stopPropagation()},id:e.colKey},null==e.startAdjustment?r.createElement(r.Fragment,null):r.createElement("div",{style:{width:5,height:"100%",position:"absolute",top:0,left:0,opacity:n?1:0,background:"#e9ecef",cursor:"col-resize"},onMouseEnter:i,onMouseLeave:c,onMouseDown:p,onClick:s}),e.sorted?r.createElement("div",{style:{position:"absolute",width:25}},e.asc?r.createElement(o.ReactIcons.ArrowDropUp,null):r.createElement(o.ReactIcons.ArrowDropDown,null)):null,r.createElement("div",{style:{marginLeft:e.sorted?25:0}},null!==(t=e.children)&&void 0!==t?t:e.colKey))},t.ColumnDataWrapper=function(e){return r.createElement("td",{style:e.style,onClick:e.onClick,draggable:null!=e.dragStart,onDragStart:e.dragStart},e.children)};const o=n(36400),r=n(8674)},7789:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return o.createElement(o.Fragment,null,e.children)},t.FilterableColumnHeader=function(e){var t,n,s,u,b,M,d,z,O,f,h,A,m,q;const[v,_]=o.useState(!1);return o.createElement(o.Fragment,null,o.createElement("div",{onMouseEnter:()=>_(!0),onMouseLeave:()=>_(!1)},o.createElement("div",{style:{marginRight:25}},e.Label),void 0!==e.Type?o.createElement(o.Fragment,null,o.createElement("div",{style:{width:25,position:"absolute",right:12,top:12}},e.Filter.length>0?o.createElement(r.ReactIcons.Filter,null):null),o.createElement("div",{style:{maxHeight:.5*window.innerHeight,overflowY:"auto",padding:"10 5",display:v?"block":"none",position:"absolute",backgroundColor:"#fff",boxShadow:"0px 8px 16px 0px rgba(0,0,0,0.2)",zIndex:401,minWidth:"calc(100% - 50px)",marginLeft:-25},"data-tableid":e.Guid,onClick:e=>{e.preventDefault(),e.stopPropagation()}},o.createElement("table",{style:{margin:0}},o.createElement("tbody",null,null!==e.ExpandedLabel&&""!==e.ExpandedLabel&&void 0!==e.ExpandedLabel?o.createElement("tr",null,o.createElement("th",{colSpan:"boolean"===e.Type?2:1},o.createElement("label",null,e.ExpandedLabel))):null,"boolean"===e.Type?o.createElement(a.BooleanFilter,{SetFilter:e.SetFilter,Filter:e.Filter,FieldName:null!==(n=null===(t=e.Field)||void 0===t?void 0:t.toString())&&void 0!==n?n:""}):null,"string"===e.Type?o.createElement(i.TextFilter,{SetFilter:e.SetFilter,Filter:e.Filter,FieldName:null!==(u=null===(s=e.Field)||void 0===s?void 0:s.toString())&&void 0!==u?u:""}):null,"enum"===e.Type&&void 0!==e.Options?o.createElement(c.EnumFilter,{FieldName:null!==(M=null===(b=e.Field)||void 0===b?void 0:b.toString())&&void 0!==M?M:"",Filter:e.Filter,SetFilter:e.SetFilter,Options:e.Options}):null,"date"===e.Type?o.createElement(l.DateFilter,{FieldName:null!==(z=null===(d=e.Field)||void 0===d?void 0:d.toString())&&void 0!==z?z:"",Filter:e.Filter,SetFilter:e.SetFilter}):null,"time"===e.Type?o.createElement(l.TimeFilter,{FieldName:null!==(f=null===(O=e.Field)||void 0===O?void 0:O.toString())&&void 0!==f?f:"",Filter:e.Filter,SetFilter:e.SetFilter}):null,"number"===e.Type?o.createElement(p.NumberFilter,{FieldName:null!==(A=null===(h=e.Field)||void 0===h?void 0:h.toString())&&void 0!==A?A:"",Filter:e.Filter,SetFilter:e.SetFilter,Unit:e.Unit}):null,"datetime"===e.Type?o.createElement(l.DateTimeFilter,{FieldName:null!==(q=null===(m=e.Field)||void 0===m?void 0:m.toString())&&void 0!==q?q:"",Filter:e.Filter,SetFilter:e.SetFilter}):null)))):null))};const o=n(8674),r=n(36400),a=n(42780),i=n(61253),c=n(52319),p=n(2228),l=n(56310)},74797:(e,t,n)=>{"use strict";var o=n(4364);Object.defineProperty(t,"__esModule",{value:!0}),t.Table=function(e){const t=r.useRef(null),n=r.useRef(new Map),i=r.useRef(0),[p,M]=r.useState(0),[d,O]=r.useState(!1),[m,q]=r.useState(0),v=r.useMemo(()=>Object.assign(Object.assign({},l),e.TableStyle),[e.TableStyle]),_=r.useMemo(()=>Object.assign(Object.assign({},s),e.TheadStyle),[e.TheadStyle]),y=r.useMemo(()=>Object.assign(Object.assign({},u),e.TbodyStyle),[e.TbodyStyle]),g=r.useMemo(()=>Object.assign(Object.assign({},b),e.RowStyle),[e.RowStyle]);r.useEffect(()=>{void 0!==e.TableStyle&&o.warn("TableStyle properties may be overridden if needed. consider using the defaults"),void 0!==e.TheadStyle&&o.warn("TheadStyle properties may be overridden if needed. consider using the defaults"),void 0!==e.TbodyStyle&&o.warn("TBodyStyle properties may be overridden if needed. consider using the defaults"),void 0!==e.RowStyle&&o.warn("RowStyle properties may be overridden if needed. consider using the defaults")},[]),r.useLayoutEffect(()=>{if(p<=0)return;const t=new Map;if(r.Children.forEach(e.children,e=>{r.isValidElement(e)&&z(e.props)&&(null!=t.get(e.props.Key)&&o.error("Multiple of the same key detected in table, this will cause issues."),t.set(e.props.Key,{minWidth:10,maxWidth:void 0,width:100}))}),p===i.current&&t.size===n.current.size&&![...t.keys()].some(e=>!n.current.has(e)))return;["minWidth","width","maxWidth"].forEach(n=>{const a=document.createElement("div");a.style.height="0px",a.style.width=`${p}px`;const i=[],c=[];r.Children.forEach(e.children,e=>{if(r.isValidElement(e)&&z(e.props)){let t=((e,t)=>{var n,o;return void 0!==(null===(n=null==e?void 0:e.RowStyle)||void 0===n?void 0:n[t])?e.RowStyle[t]:void 0!==(null===(o=null==e?void 0:e.HeaderStyle)||void 0===o?void 0:o[t])?e.HeaderStyle[t]:void 0})(e.props,n);if("width"===n&&null==t&&(t="auto"),null!=t)if("auto"===t)i.push(e.props.Key);else{const n=document.createElement("div");n.id=e.props.Key+"_measurement",n.style.height="0px",null!=(null==t?void 0:t.length)?n.style.width=t:n.style.width=`${t}px`,a.appendChild(n),c.push(e.props.Key)}}}),document.body.appendChild(a);let l=p;if(c.forEach(e=>{const r=document.getElementById(e+"_measurement"),a=null==r?void 0:r.getBoundingClientRect().width;if(null!=a){const r=t.get(e);null!=r?(l-=a,r[n]=a):o.error("Could not find width object for Key: "+e)}else o.error("Could not find measurement div with Key: "+e)}),document.body.removeChild(a),"width"===n&&i.length>0){const e=l/i.length;i.forEach(r=>{const a=t.get(r);null!=a?a[n]=e:o.error("Could not find width object for Key: "+r)})}});let a=p;[...t.keys()].forEach(e=>{const n=t.get(e);null!=n?n.minWidth<=a?(n.minWidth>n.width&&(n.width=n.minWidth),null!=n.maxWidth&&n.minWidth>n.maxWidth&&(n.maxWidth=n.minWidth),null!=n.maxWidth&&n.width>n.maxWidth&&(n.width=n.maxWidth),n.width>a&&(n.width=a),a-=n.width):(n.minWidth=0,n.width=0,n.maxWidth=0):o.error("Could not find width object for Key: "+e)}),n.current=t,i.current=p,q(e=>e+1)},[e.children,p]);const W=r.useCallback(a.debounce(()=>{var n,o,r,a;if(null==t.current)return;let i=!1;const p=null===(n=t.current)||void 0===n?void 0:n.getBoundingClientRect();i="scroll"===(null===(o=e.TbodyStyle)||void 0===o?void 0:o.overflowY)||"scroll"===(null===(r=e.TbodyStyle)||void 0===r?void 0:r.overflow)||(null==p?void 0:p.height){let e;const n=setInterval(()=>{null!=(null==t?void 0:t.current)&&(e=new ResizeObserver(()=>{W()}),e.observe(t.current),clearInterval(n))},10);return()=>{clearInterval(n),null!=e&&null!=e.disconnect&&e.disconnect()}},[]);const L=r.useCallback((t,n)=>{null!==t.colKey&&e.OnSort(t,n)},[e.OnSort]);return r.createElement("table",{className:void 0!==e.TableClass?e.TableClass:"table table-hover",style:v},r.createElement(A,{Class:e.TheadClass,Style:_,SortKey:e.SortKey,Ascending:e.Ascending,LastColumn:e.LastColumn,OnSort:L,ColWidths:n,SetFilters:e.SetFilters,Filters:e.Filters,Trigger:m,TriggerRerender:()=>q(e=>e+1)},e.children),r.createElement(h,{DragStart:e.OnDragStart,Data:e.Data,RowStyle:g,BodyStyle:y,BodyClass:e.TbodyClass,OnClick:e.OnClick,Selected:e.Selected,KeySelector:e.KeySelector,BodyRef:t,BodyScrolled:d,ColWidths:n,Trigger:m},e.children),void 0!==e.LastRow?r.createElement("tfoot",{style:e.TfootStyle,className:e.TfootClass},r.createElement("tr",{style:void 0!==e.RowStyle?Object.assign({},e.RowStyle):{}},e.LastRow)):null)};const r=n(8674),a=n(42005),i=n(45451),c=n(76752),p=n(7789),l={padding:0,flex:1,tableLayout:"fixed",overflow:"hidden",display:"flex",flexDirection:"column",marginBottom:0,width:"100%"},s={fontSize:"auto",tableLayout:"fixed",display:"table",width:"100%"},u={flex:1,display:"block",overflow:"auto"},b={display:"table",tableLayout:"fixed",width:"100%"},M={display:"inline-block",position:"relative",borderTop:"none",width:"auto"},d={overflowX:"hidden",display:"inline-block",width:"auto"},z=e=>null!=(null==e?void 0:e.Key),O=e=>{const t=null==e?void 0:e.Adjustable;return(!1===t||!0===t)&&t},f=17;function h(e){const t=r.useMemo(()=>Object.assign(Object.assign({},e.BodyStyle),{display:"block"}),[e.BodyStyle]),n=r.useCallback((t,n,o)=>{void 0!==e.OnClick&&e.OnClick({colKey:void 0,colField:void 0,row:n,data:null,index:o},t)},[e.OnClick]);return r.createElement("tbody",{style:t,className:e.BodyClass,ref:e.BodyRef},e.Data.map((t,o)=>{const a=void 0!==e.RowStyle?Object.assign({},e.RowStyle):{};void 0!==a.cursor||void 0===e.OnClick&&void 0===e.DragStart||(a.cursor="pointer"),void 0!==e.Selected&&e.Selected(t,o)&&(a.backgroundColor="var(--warning)");const c=e.KeySelector(t,o);return r.createElement("tr",{key:c,style:a,onClick:e=>n(e,t,o)},r.Children.map(e.children,n=>{var a,c,p,l,s,u;if(!r.isValidElement(n))return null;if(!z(n.props))return null;const b=e.ColWidths.current.get(n.props.Key);if(null==b||0===b.width)return null;let M;null!=(null===(c=null===(a=n.props)||void 0===a?void 0:a.RowStyle)||void 0===c?void 0:c.cursor)?M=n.props.RowStyle.cursor:null!=(null==e?void 0:e.OnClick)?M="pointer":null!=(null==e?void 0:e.DragStart)&&(M="grab");const O=Object.assign(Object.assign(Object.assign({},d),null===(p=n.props)||void 0===p?void 0:p.RowStyle),{width:b.width,cursor:M});return r.createElement(i.ColumnDataWrapper,{key:n.key,onClick:r=>{var a,i;if(null!=e.OnClick)return e.OnClick({colKey:n.props.Key,colField:null===(a=n.props)||void 0===a?void 0:a.Field,row:t,data:t[null===(i=n.props)||void 0===i?void 0:i.Field],index:o},r)},dragStart:null==e.DragStart?void 0:r=>{var a,i;if(null!=e.DragStart)return e.DragStart({colKey:n.props.Key,colField:null===(a=n.props)||void 0===a?void 0:a.Field,row:t,data:t[null===(i=n.props)||void 0===i?void 0:i.Field],index:o},r)},style:O},null!=(null===(l=n.props)||void 0===l?void 0:l.Content)?n.props.Content({item:t,key:n.props.Key,field:null===(s=n.props)||void 0===s?void 0:s.Field,style:O,index:o}):null!=(null===(u=n.props)||void 0===u?void 0:u.Field)?t[n.props.Field]:null)}))}))}function A(e){var t;const n=r.useMemo(()=>Object.assign(Object.assign({},s),e.Style),[e.Style]),[a,l]=r.useState(0),[u,b]=r.useState(void 0),[d,h]=r.useState(0),[A,m]=r.useState({min:-1/0,max:1/0}),[q,v]=r.useState(null!==(t=e.Filters)&&void 0!==t?t:[]),[_]=r.useState((0,c.CreateGuid)()),y=r.useCallback((t,n)=>{var o,a;const i=null!==(o=e.children)&&void 0!==o?o:[],c=(null!==(a=r.Children.map(i,e=>{var o;if(!r.isValidElement(e))return null;const a=null===(o=n.current.get(t))||void 0===o?void 0:o.width;return null==a||a<=0?null:z(e.props)&&O(e.props)?e.props.Key:null}))&&void 0!==a?a:[]).filter(e=>null!==e),p=c.indexOf(t);if(!(p<=0))return c[p-1]},[e.children]),g=r.useCallback((e,t)=>{if(void 0===e)return{min:-1/0,max:1/0};const n=t.current.get(e[0]),o=t.current.get(e[1]);if(null==n||null==o)return{min:-1/0,max:1/0};const r=n.width-n.minWidth,a=null==n.maxWidth?Number.MAX_SAFE_INTEGER:n.maxWidth-n.width,i=o.width-o.minWidth,c=null==o.maxWidth?Number.MAX_SAFE_INTEGER:o.maxWidth-o.width;return{min:-(r0===e?1:1===e?-1:0,[]),L=r.useCallback((e,t,n)=>{const r=g(t,n);let a;if(a=e>r.max?r.max:e5){const e=n.current.get(t[0]),r=n.current.get(t[1]);null==e||null==r?o.error(`Unable to finalize adjustment on keys ${t[0]}, ${t[1]}`):(e.width+=W(0)*a,r.width+=W(1)*a)}l(0),m({min:-1/0,max:1/0}),b(void 0),h(0)},[g,W]),R=r.useCallback(e=>{if(void 0===u)return;const t=e.screenX-a;h(t)},[a,u]),w=r.useCallback((t,n)=>{v(r=>{const a=r.filter(e=>e.FieldName!==n).concat(t);return null==e.SetFilters?o.error("Attempted to set filters from column when no set filter arguement to table was provided. Data has not been filtered!"):e.SetFilters(a),a})},[e.SetFilters]);return r.useEffect(()=>{var t;return v(null!==(t=e.Filters)&&void 0!==t?t:[])},[e.Filters]),r.createElement("thead",{className:e.Class,style:n,onMouseMove:e=>{R(e.nativeEvent),e.stopPropagation()},onMouseUp:t=>{t.stopPropagation(),null!=u&&(L(d,u,e.ColWidths),e.TriggerRerender())},onMouseLeave:t=>{t.stopPropagation(),null!=u&&(L(d,u,e.ColWidths),e.TriggerRerender())}},r.createElement("tr",{style:{width:"100%",display:"table"}},r.Children.map(e.children,t=>{var n,o,a,c,s,f,v,L,R,S,x,k,T,N,E,B;if(!r.isValidElement(t))return null;if(!z(t.props))return null;const D=e.ColWidths.current.get(t.props.Key);if(null==D||0===D.width)return null;let P=D.width;const C=null!==(n=null==u?void 0:u.indexOf(t.props.Key))&&void 0!==n?n:-1;if(C>-1){let e;e=d>A.max?A.max:d{const o=y(t.props.Key,e.ColWidths);if(null!=o){const r=[o,t.props.Key];b(r),l(n.screenX),m(g(r,e.ColWidths)),h(0)}}),r.createElement(i.ColumnHeaderWrapper,{onSort:n=>{var o;return e.OnSort({colKey:t.props.Key,colField:null===(o=t.props)||void 0===o?void 0:o.Field,ascending:e.Ascending},n)},sorted:e.SortKey===t.props.Key&&(null===(L=null===(v=t.props)||void 0===v?void 0:v.AllowSort)||void 0===L||L),asc:e.Ascending,colKey:t.props.Key,key:t.props.Key,allowSort:null===(R=t.props)||void 0===R?void 0:R.AllowSort,startAdjustment:X,style:Y}," ",t.type===p.default?r.createElement(p.FilterableColumnHeader,{Label:null===(S=t.props)||void 0===S?void 0:S.children,Filter:q.filter(e=>{var n,o;return e.FieldName===(null===(o=null===(n=t.props)||void 0===n?void 0:n.Field)||void 0===o?void 0:o.toString())}),SetFilter:e=>{var n;return w(e,null===(n=t.props)||void 0===n?void 0:n.Field)},Field:null===(x=t.props)||void 0===x?void 0:x.Field,Type:null===(k=t.props)||void 0===k?void 0:k.Type,Options:null===(T=t.props)||void 0===T?void 0:T.Enum,ExpandedLabel:null===(N=t.props)||void 0===N?void 0:N.ExpandedLabel,Guid:_,Unit:null===(E=t.props)||void 0===E?void 0:E.Unit}):null!==(B=t.props.children)&&void 0!==B?B:t.props.Key," ")}),void 0!==e.LastColumn?r.createElement("th",{style:{width:f,padding:0,maxWidth:f}},e.LastColumn):null))}},22772:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},23514:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Paging=t.ConfigurableColumn=t.ConfigurableTable=t.FilterableColumn=t.Column=t.Table=t.ReactTableProps=void 0;const o=n(69928);t.Paging=o.default;const r=n(22772);t.ReactTableProps=r;const a=n(74797);Object.defineProperty(t,"Table",{enumerable:!0,get:function(){return a.Table}});const i=n(45451);Object.defineProperty(t,"Column",{enumerable:!0,get:function(){return i.Column}});const c=n(7789);t.FilterableColumn=c.default;const p=n(81201);t.ConfigurableTable=p.default;const l=n(49815);t.ConfigurableColumn=l.default},34237:(e,t,n)=>{"use strict";var o,r=Object.assign||function(e){for(var t=1;t=0||Object.prototype.hasOwnProperty.call(e,o)&&(n[o]=e[o]);return n}(e,["fill","width","height","style"]);return a.default.createElement("svg",r({viewBox:"0 0 24 24",style:r({fill:n,width:i,height:p},s)},u),a.default.createElement("path",{d:"M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"}))}},32546:(e,t,n)=>{"use strict";var o,r=Object.assign||function(e){for(var t=1;t=0||Object.prototype.hasOwnProperty.call(e,o)&&(n[o]=e[o]);return n}(e,["fill","width","height","style"]);return a.default.createElement("svg",r({viewBox:"0 0 24 24",style:r({fill:n,width:i,height:p},s)},u),a.default.createElement("path",{d:"M12,18.17L8.83,15L7.42,16.41L12,21L16.59,16.41L15.17,15M12,5.83L15.17,9L16.58,7.59L12,3L7.41,7.59L8.83,9L12,5.83Z"}))}},98e3:(e,t,n)=>{"use strict";function o(e){for(var t=arguments.length,n=Array(t>1?t-1:0),o=1;o3?t.i-4:t.i:Array.isArray(e)?1:b(e)?2:M(e)?3:0}function l(e,t){return 2===p(e)?e.has(t):Object.prototype.hasOwnProperty.call(e,t)}function s(e,t,n){var o=p(e);2===o?e.set(t,n):3===o?e.add(n):e[t]=n}function u(e,t){return e===t?0!==e||1/e==1/t:e!=e&&t!=t}function b(e){return j&&e instanceof Map}function M(e){return Y&&e instanceof Set}function d(e){return e.o||e.t}function z(e){if(Array.isArray(e))return Array.prototype.slice.call(e);var t=V(e);delete t[I];for(var n=G(t),o=0;o1&&(e.set=e.add=e.clear=e.delete=f),Object.freeze(e),t&&c(e,function(e,t){return O(t,!0)},!0)),e}function f(){o(2)}function h(e){return null==e||"object"!=typeof e||Object.isFrozen(e)}function A(e){var t=K[e];return t||o(18,e),t}function m(){return P}function q(e,t){t&&(A("Patches"),e.u=[],e.s=[],e.v=t)}function v(e){_(e),e.p.forEach(g),e.p=null}function _(e){e===P&&(P=e.l)}function y(e){return P={p:[],l:P,h:e,m:!0,_:0}}function g(e){var t=e[I];0===t.i||1===t.i?t.j():t.g=!0}function W(e,t){t._=t.p.length;var n=t.p[0],r=void 0!==e&&e!==n;return t.h.O||A("ES5").S(t,e,r),r?(n[I].P&&(v(t),o(4)),a(e)&&(e=L(t,e),t.l||w(t,e)),t.u&&A("Patches").M(n[I].t,e,t.u,t.s)):e=L(t,n,[]),v(t),t.u&&t.v(t.u,t.s),e!==F?e:void 0}function L(e,t,n){if(h(t))return t;var o=t[I];if(!o)return c(t,function(r,a){return R(e,o,t,r,a,n)},!0),t;if(o.A!==e)return t;if(!o.P)return w(e,o.t,!0),o.t;if(!o.I){o.I=!0,o.A._--;var r=4===o.i||5===o.i?o.o=z(o.k):o.o,a=r,i=!1;3===o.i&&(a=new Set(r),r.clear(),i=!0),c(a,function(t,a){return R(e,o,r,t,a,n,i)}),w(e,r,!1),n&&e.u&&A("Patches").N(o,n,e.u,e.s)}return o.o}function R(e,t,n,o,i,c,p){if(r(i)){var u=L(e,i,c&&t&&3!==t.i&&!l(t.R,o)?c.concat(o):void 0);if(s(n,o,u),!r(u))return;e.m=!1}else p&&n.add(i);if(a(i)&&!h(i)){if(!e.h.D&&e._<1)return;L(e,i),t&&t.A.l||w(e,i)}}function w(e,t,n){void 0===n&&(n=!1),!e.l&&e.h.D&&e.m&&O(t,n)}function S(e,t){var n=e[I];return(n?d(n):e)[t]}function x(e,t){if(t in e)for(var n=Object.getPrototypeOf(e);n;){var o=Object.getOwnPropertyDescriptor(n,t);if(o)return o;n=Object.getPrototypeOf(n)}}function k(e){e.P||(e.P=!0,e.l&&k(e.l))}function T(e){e.o||(e.o=z(e.t))}function N(e,t,n){var o=b(t)?A("MapSet").F(t,n):M(t)?A("MapSet").T(t,n):e.O?function(e,t){var n=Array.isArray(e),o={i:n?1:0,A:t?t.A:m(),P:!1,I:!1,R:{},l:t,t:e,k:null,o:null,j:null,C:!1},r=o,a=Z;n&&(r=[o],a=J);var i=Proxy.revocable(r,a),c=i.revoke,p=i.proxy;return o.k=p,o.j=c,p}(t,n):A("ES5").J(t,n);return(n?n.A:m()).p.push(o),o}function E(e){return r(e)||o(22,e),function e(t){if(!a(t))return t;var n,o=t[I],r=p(t);if(o){if(!o.P&&(o.i<4||!A("ES5").K(o)))return o.t;o.I=!0,n=B(t,r),o.I=!1}else n=B(t,r);return c(n,function(t,r){o&&function(e,t){return 2===p(e)?e.get(t):e[t]}(o.t,t)===r||s(n,t,e(r))}),3===r?new Set(n):n}(e)}function B(e,t){switch(t){case 2:return new Map(e);case 3:return Array.from(e)}return z(e)}n.r(t),n.d(t,{MiddlewareArray:()=>Fe,TaskAbortError:()=>jt,__DO_NOT_USE__ActionTypes:()=>le,addListener:()=>en,applyMiddleware:()=>Oe,bindActionCreators:()=>de,clearAllListeners:()=>tn,combineReducers:()=>be,compose:()=>ze,configureStore:()=>Qe,createAction:()=>$e,createAsyncThunk:()=>Ot,createDraftSafeSelector:()=>je,createEntityAdapter:()=>st,createImmutableStateInvariantMiddleware:()=>Ue,createListenerMiddleware:()=>an,createNextState:()=>te,createReducer:()=>ot,createSelector:()=>qe,createSerializableStateInvariantMiddleware:()=>Ke,createSlice:()=>rt,createStore:()=>se,current:()=>E,findNonSerializableValue:()=>Ve,freeze:()=>O,getDefaultMiddleware:()=>Ze,getType:()=>tt,isAllOf:()=>mt,isAnyOf:()=>At,isAsyncThunkAction:()=>Lt,isDraft:()=>r,isFulfilled:()=>Wt,isImmutableDefault:()=>Ie,isPending:()=>_t,isPlain:()=>Ge,isPlainObject:()=>Xe,isRejected:()=>yt,isRejectedWithValue:()=>gt,legacy_createStore:()=>ue,miniSerializeError:()=>zt,nanoid:()=>ut,original:()=>i,removeListener:()=>nn,unwrapResult:()=>ft});var D,P,C="undefined"!=typeof Symbol&&"symbol"==typeof Symbol("x"),j="undefined"!=typeof Map,Y="undefined"!=typeof Set,X="undefined"!=typeof Proxy&&void 0!==Proxy.revocable&&"undefined"!=typeof Reflect,F=C?Symbol.for("immer-nothing"):((D={})["immer-nothing"]=!0,D),H=C?Symbol.for("immer-draftable"):"__$immer_draftable",I=C?Symbol.for("immer-state"):"__$immer_state",U=("undefined"!=typeof Symbol&&Symbol.iterator,""+Object.prototype.constructor),G="undefined"!=typeof Reflect&&Reflect.ownKeys?Reflect.ownKeys:void 0!==Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:Object.getOwnPropertyNames,V=Object.getOwnPropertyDescriptors||function(e){var t={};return G(e).forEach(function(n){t[n]=Object.getOwnPropertyDescriptor(e,n)}),t},K={},Z={get:function(e,t){if(t===I)return e;var n=d(e);if(!l(n,t))return function(e,t,n){var o,r=x(t,n);return r?"value"in r?r.value:null===(o=r.get)||void 0===o?void 0:o.call(e.k):void 0}(e,n,t);var o=n[t];return e.I||!a(o)?o:o===S(e.t,t)?(T(e),e.o[t]=N(e.A.h,o,e)):o},has:function(e,t){return t in d(e)},ownKeys:function(e){return Reflect.ownKeys(d(e))},set:function(e,t,n){var o=x(d(e),t);if(null==o?void 0:o.set)return o.set.call(e.k,n),!0;if(!e.P){var r=S(d(e),t),a=null==r?void 0:r[I];if(a&&a.t===n)return e.o[t]=n,e.R[t]=!1,!0;if(u(n,r)&&(void 0!==n||l(e.t,t)))return!0;T(e),k(e)}return e.o[t]===n&&(void 0!==n||t in e.o)||Number.isNaN(n)&&Number.isNaN(e.o[t])||(e.o[t]=n,e.R[t]=!0),!0},deleteProperty:function(e,t){return void 0!==S(e.t,t)||t in e.t?(e.R[t]=!1,T(e),k(e)):delete e.R[t],e.o&&delete e.o[t],!0},getOwnPropertyDescriptor:function(e,t){var n=d(e),o=Reflect.getOwnPropertyDescriptor(n,t);return o?{writable:!0,configurable:1!==e.i||"length"!==t,enumerable:o.enumerable,value:n[t]}:o},defineProperty:function(){o(11)},getPrototypeOf:function(e){return Object.getPrototypeOf(e.t)},setPrototypeOf:function(){o(12)}},J={};c(Z,function(e,t){J[e]=function(){return arguments[0]=arguments[0][0],t.apply(this,arguments)}}),J.deleteProperty=function(e,t){return J.set.call(this,e,t,void 0)},J.set=function(e,t,n){return Z.set.call(this,e[0],t,n,e[0])};var Q=function(){function e(e){var t=this;this.O=X,this.D=!0,this.produce=function(e,n,r){if("function"==typeof e&&"function"!=typeof n){var i=n;n=e;var c=t;return function(e){var t=this;void 0===e&&(e=i);for(var o=arguments.length,r=Array(o>1?o-1:0),a=1;a1?o-1:0),a=1;a=0;n--){var o=t[n];if(0===o.path.length&&"replace"===o.op){e=o.value;break}}n>-1&&(t=t.slice(n+1));var a=A("Patches").$;return r(e)?a(e,t):this.produce(e,function(e){return a(e,t)})},e}(),$=new Q,ee=$.produce;$.produceWithPatches.bind($),$.setAutoFreeze.bind($),$.setUseProxies.bind($),$.applyPatches.bind($),$.createDraft.bind($),$.finishDraft.bind($);const te=ee;function ne(e){return ne="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},ne(e)}function oe(e,t,n){return(t=function(e){var t=function(e){if("object"!=ne(e)||!e)return e;var t=e[Symbol.toPrimitive];if(void 0!==t){var n=t.call(e,"string");if("object"!=ne(n))return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}(e);return"symbol"==ne(t)?t:t+""}(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function re(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,o)}return n}function ae(e){for(var t=1;t-1){var r=n[o];return o>0&&(n.splice(o,1),n.unshift(r)),r.value}return fe}return{get:o,put:function(t,r){o(t)===fe&&(n.unshift({key:t,value:r}),n.length>e&&n.pop())},getEntries:function(){return n},clear:function(){n=[]}}}(p,s);function b(){var t=u.get(arguments);if(t===fe){if(t=e.apply(null,arguments),l){var n=u.getEntries().find(function(e){return l(e.value,t)});n&&(t=n.value)}u.put(arguments,t)}return t}return b.clearCache=function(){return u.clear()},b}function me(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),o=1;o0&&r[r.length-1])||6!==a[0]&&2!==a[0])){i=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]",value:e};if("object"!=typeof e||null===e)return!1;for(var i=null!=o?o(e):Object.entries(e),c=r.length>0,p=0,l=i;p=0)){if(!n(b))return{keyPath:M,value:b};if("object"==typeof b&&(a=Ve(b,M,n,o,r)))return a}}return!1}function Ke(e){return void 0===e&&(e={}),function(){return function(e){return function(t){return e(t)}}}}function Ze(e){void 0===e&&(e={});var t=e.thunk,n=void 0===t||t,o=(e.immutableCheck,e.serializableCheck,new Fe);return n&&(function(e){return"boolean"==typeof e}(n)?o.push(ye):o.push(ye.withExtraArgument(n.extraArgument))),o}var Je=!0;function Qe(e){var t,n=function(e){return Ze(e)},o=e||{},r=o.reducer,a=void 0===r?void 0:r,i=o.middleware,c=void 0===i?n():i,p=o.devTools,l=void 0===p||p,s=o.preloadedState,u=void 0===s?void 0:s,b=o.enhancers,M=void 0===b?void 0:b;if("function"==typeof a)t=a;else{if(!Xe(a))throw new Error('"reducer" is a required argument, and must be a function or an object of functions that can be passed to combineReducers');t=be(a)}var d=c;if("function"==typeof d&&(d=d(n),!Je&&!Array.isArray(d)))throw new Error("when using a middleware builder function, an array of middleware must be returned");if(!Je&&d.some(function(e){return"function"!=typeof e}))throw new Error("each middleware provided to configureStore must be a function");var z=Oe.apply(void 0,d),O=ze;l&&(O=Ye(De({trace:!Je},"object"==typeof l&&l)));var f=[z];return Array.isArray(M)?f=we([z],M):"function"==typeof M&&(f=M(f)),se(t,u,O.apply(void 0,f))}function $e(e,t){function n(){for(var n=[],o=0;o-1}function tt(e){return""+e}function nt(e){var t,n={},o=[],r={addCase:function(e,t){var o="string"==typeof e?e:e.type;if(o in n)throw new Error("addCase cannot be called with two reducers for the same action type");return n[o]=t,r},addMatcher:function(e,t){return o.push({matcher:e,reducer:t}),r},addDefaultCase:function(e){return t=e,r}};return e(r),[n,o,t]}function ot(e,t,n,o){void 0===n&&(n=[]);var i,c="function"==typeof t?nt(t):[t,n,o],p=c[0],l=c[1],s=c[2];if(function(e){return"function"==typeof e}(e))i=function(){return He(e())};else{var u=He(e);i=function(){return u}}function b(e,t){void 0===e&&(e=i());var n=we([p[t.type]],l.filter(function(e){return(0,e.matcher)(t)}).map(function(e){return e.reducer}));return 0===n.filter(function(e){return!!e}).length&&(n=[s]),n.reduce(function(e,n){if(n){var o;if(r(e))return void 0===(o=n(e,t))?e:o;if(a(e))return te(e,function(e){return n(e,t)});if(void 0===(o=n(e,t))){if(null===e)return e;throw Error("A case reducer on a non-draftable value must not return undefined")}return o}return e},e)}return b.getInitialState=i,b}function rt(e){var t=e.name;if(!t)throw new Error("`name` is a required option for createSlice");var n,o="function"==typeof e.initialState?e.initialState:He(e.initialState),r=e.reducers||{},a=Object.keys(r),i={},c={},p={};function l(){var t="function"==typeof e.extraReducers?nt(e.extraReducers):[e.extraReducers],n=t[0],r=void 0===n?{}:n,a=t[1],i=void 0===a?[]:a,p=t[2],l=void 0===p?void 0:p,s=De(De({},r),c);return ot(o,s,i,l)}return a.forEach(function(e){var n,o,a=r[e],l=t+"/"+e;"reducer"in a?(n=a.reducer,o=a.prepare):n=a,i[e]=n,c[l]=n,p[e]=o?$e(l,o):$e(l)}),{name:t,reducer:function(e,t){return n||(n=l()),n(e,t)},actions:p,caseReducers:i,getInitialState:function(){return n||(n=l()),n.getInitialState()}}}function at(e){return function(t,n){var o=function(t){var o;Xe(o=n)&&"string"==typeof o.type&&Object.keys(o).every(et)?e(n.payload,t):e(n,t)};return r(t)?(o(t),t):te(t,o)}}function it(e,t){return t(e)}function ct(e){return Array.isArray(e)||(e=Object.values(e)),e}function pt(e,t,n){for(var o=[],r=[],a=0,i=e=ct(e);a0){var a=t.filter(function(t){return function(t,n,o){var r=o.entities[n.id],a=Object.assign({},r,n.changes),i=it(a,e),c=i!==n.id;return c&&(t[n.id]=i,delete o.entities[n.id]),o.entities[i]=a,c}(o,t,n)}).length>0;a&&(n.ids=n.ids.map(function(e){return o[e]||e}))}}function i(t,o){var r=pt(t,e,o),i=r[0];a(r[1],o),n(i,o)}return{removeAll:(c=function(e){Object.assign(e,{ids:[],entities:{}})},p=at(function(e,t){return c(t)}),function(e){return p(e,void 0)}),addOne:at(t),addMany:at(n),setOne:at(o),setMany:at(function(e,t){for(var n=0,r=e=ct(e);n-1;return n&&o}function vt(e){return"function"==typeof e[0]&&"pending"in e[0]&&"fulfilled"in e[0]&&"rejected"in e[0]}function _t(){for(var e=[],t=0;t0)for(var b=e.getState(),M=Array.from(n.values()),d=0,z=M;d=0;t--){var r=e[t][I];if(!r.P)switch(r.i){case 5:o(r)&&k(r);break;case 4:n(r)&&k(r)}}}function n(e){for(var t=e.t,n=e.k,o=G(n),r=o.length-1;r>=0;r--){var a=o[r];if(a!==I){var i=t[a];if(void 0===i&&!l(t,a))return!0;var c=n[a],p=c&&c[I];if(p?p.t!==i:!u(c,i))return!0}}var s=!!t[I];return o.length!==G(t).length+(s?0:1)}function o(e){var t=e.k;if(t.length!==e.t.length)return!0;var n=Object.getOwnPropertyDescriptor(t,t.length-1);if(n&&!n.get)return!0;for(var o=0;o{"use strict";var o=n(39907),r=n(4364);function a(e){return a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a(e)}function i(e,t){for(var n=0;n1?n-1:0),r=1;r1?n-1:0),r=1;r1?n-1:0),r=1;r1?n-1:0),r=1;r{"use strict";var o=n(39907);function r(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,o)}return n}function a(e){for(var t=1;te.length)&&(n=e.length),e.substring(n-t.length,n)===t}var A="",m="",q="",v="",_={deepStrictEqual:"Expected values to be strictly deep-equal:",strictEqual:"Expected values to be strictly equal:",strictEqualObject:'Expected "actual" to be reference-equal to "expected":',deepEqual:"Expected values to be loosely deep-equal:",equal:"Expected values to be loosely equal:",notDeepStrictEqual:'Expected "actual" not to be strictly deep-equal to:',notStrictEqual:'Expected "actual" to be strictly unequal to:',notStrictEqualObject:'Expected "actual" not to be reference-equal to "expected":',notDeepEqual:'Expected "actual" not to be loosely deep-equal to:',notEqual:'Expected "actual" to be loosely unequal to:',notIdentical:"Values identical but not reference-equal:"};function y(e){var t=Object.keys(e),n=Object.create(Object.getPrototypeOf(e));return t.forEach(function(t){n[t]=e[t]}),Object.defineProperty(n,"message",{value:e.message}),n}function g(e){return O(e,{compact:!1,customInspect:!1,depth:1e3,maxArrayLength:1/0,showHidden:!1,breakLength:1/0,showProxy:!1,sorted:!0,getters:!0})}var W=function(e,t){!function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&M(e,t)}(W,e);var n,r,c,s,u=(n=W,r=b(),function(){var e,t=d(n);if(r){var o=d(this).constructor;e=Reflect.construct(t,arguments,o)}else e=t.apply(this,arguments);return p(this,e)});function W(e){var t;if(function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,W),"object"!==z(e)||null===e)throw new f("options","Object",e);var n=e.message,r=e.operator,a=e.stackStartFn,i=e.actual,c=e.expected,s=Error.stackTraceLimit;if(Error.stackTraceLimit=0,null!=n)t=u.call(this,String(n));else if(o.stderr&&o.stderr.isTTY&&(o.stderr&&o.stderr.getColorDepth&&1!==o.stderr.getColorDepth()?(A="",m="",v="",q=""):(A="",m="",v="",q="")),"object"===z(i)&&null!==i&&"object"===z(c)&&null!==c&&"stack"in i&&i instanceof Error&&"stack"in c&&c instanceof Error&&(i=y(i),c=y(c)),"deepStrictEqual"===r||"strictEqual"===r)t=u.call(this,function(e,t,n){var r="",a="",i=0,c="",p=!1,l=g(e),s=l.split("\n"),u=g(t).split("\n"),b=0,M="";if("strictEqual"===n&&"object"===z(e)&&"object"===z(t)&&null!==e&&null!==t&&(n="strictEqualObject"),1===s.length&&1===u.length&&s[0]!==u[0]){var d=s[0].length+u[0].length;if(d<=10){if(!("object"===z(e)&&null!==e||"object"===z(t)&&null!==t||0===e&&0===t))return"".concat(_[n],"\n\n")+"".concat(s[0]," !== ").concat(u[0],"\n")}else if("strictEqualObject"!==n&&d<(o.stderr&&o.stderr.isTTY?o.stderr.columns:80)){for(;s[0][b]===u[0][b];)b++;b>2&&(M="\n ".concat(function(e,t){if(t=Math.floor(t),0==e.length||0==t)return"";var n=e.length*t;for(t=Math.floor(Math.log(t)/Math.log(2));t;)e+=e,t--;return e+e.substring(0,n-e.length)}(" ",b),"^"),b=0)}}for(var O=s[s.length-1],f=u[u.length-1];O===f&&(b++<2?c="\n ".concat(O).concat(c):r=O,s.pop(),u.pop(),0!==s.length&&0!==u.length);)O=s[s.length-1],f=u[u.length-1];var y=Math.max(s.length,u.length);if(0===y){var W=l.split("\n");if(W.length>30)for(W[26]="".concat(A,"...").concat(v);W.length>27;)W.pop();return"".concat(_.notIdentical,"\n\n").concat(W.join("\n"),"\n")}b>3&&(c="\n".concat(A,"...").concat(v).concat(c),p=!0),""!==r&&(c="\n ".concat(r).concat(c),r="");var L=0,R=_[n]+"\n".concat(m,"+ actual").concat(v," ").concat(q,"- expected").concat(v),w=" ".concat(A,"...").concat(v," Lines skipped");for(b=0;b1&&b>2&&(S>4?(a+="\n".concat(A,"...").concat(v),p=!0):S>3&&(a+="\n ".concat(u[b-2]),L++),a+="\n ".concat(u[b-1]),L++),i=b,r+="\n".concat(q,"-").concat(v," ").concat(u[b]),L++;else if(u.length1&&b>2&&(S>4?(a+="\n".concat(A,"...").concat(v),p=!0):S>3&&(a+="\n ".concat(s[b-2]),L++),a+="\n ".concat(s[b-1]),L++),i=b,a+="\n".concat(m,"+").concat(v," ").concat(s[b]),L++;else{var x=u[b],k=s[b],T=k!==x&&(!h(k,",")||k.slice(0,-1)!==x);T&&h(x,",")&&x.slice(0,-1)===k&&(T=!1,k+=","),T?(S>1&&b>2&&(S>4?(a+="\n".concat(A,"...").concat(v),p=!0):S>3&&(a+="\n ".concat(s[b-2]),L++),a+="\n ".concat(s[b-1]),L++),i=b,a+="\n".concat(m,"+").concat(v," ").concat(k),r+="\n".concat(q,"-").concat(v," ").concat(x),L+=2):(a+=r,r="",1!==S&&0!==b||(a+="\n ".concat(k),L++))}if(L>20&&b30)for(M[26]="".concat(A,"...").concat(v);M.length>27;)M.pop();t=1===M.length?u.call(this,"".concat(b," ").concat(M[0])):u.call(this,"".concat(b,"\n\n").concat(M.join("\n"),"\n"))}else{var d=g(i),O="",L=_[r];"notDeepEqual"===r||"notEqual"===r?(d="".concat(_[r],"\n\n").concat(d)).length>1024&&(d="".concat(d.slice(0,1021),"...")):(O="".concat(g(c)),d.length>512&&(d="".concat(d.slice(0,509),"...")),O.length>512&&(O="".concat(O.slice(0,509),"...")),"deepEqual"===r||"equal"===r?d="".concat(L,"\n\n").concat(d,"\n\nshould equal\n\n"):O=" ".concat(r," ").concat(O)),t=u.call(this,"".concat(d).concat(O))}return Error.stackTraceLimit=s,t.generatedMessage=!n,Object.defineProperty(l(t),"name",{value:"AssertionError [ERR_ASSERTION]",enumerable:!1,writable:!0,configurable:!0}),t.code="ERR_ASSERTION",t.actual=i,t.expected=c,t.operator=r,Error.captureStackTrace&&Error.captureStackTrace(l(t),a),t.stack,t.name="AssertionError",p(t)}return c=W,(s=[{key:"toString",value:function(){return"".concat(this.name," [").concat(this.code,"]: ").concat(this.message)}},{key:t,value:function(e,t){return O(this,a(a({},t),{},{customInspect:!1,depth:0}))}}])&&i(c.prototype,s),Object.defineProperty(c,"prototype",{writable:!1}),W}(s(Error),O.custom);e.exports=W},41342:(e,t,n)=>{"use strict";function o(e){return o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},o(e)}function r(e,t){return r=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},r(e,t)}function a(e){return a=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},a(e)}var i,c,p={};function l(e,t,n){n||(n=Error);var i=function(n){!function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&r(e,t)}(s,n);var i,c,p,l=(c=s,p=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch(e){return!1}}(),function(){var e,t=a(c);if(p){var n=a(this).constructor;e=Reflect.construct(t,arguments,n)}else e=t.apply(this,arguments);return function(e,t){if(t&&("object"===o(t)||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e)}(this,e)});function s(n,o,r){var a;return function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,s),a=l.call(this,function(e,n,o){return"string"==typeof t?t:t(e,n,o)}(n,o,r)),a.code=e,a}return i=s,Object.defineProperty(i,"prototype",{writable:!1}),i}(n);p[e]=i}function s(e,t){if(Array.isArray(e)){var n=e.length;return e=e.map(function(e){return String(e)}),n>2?"one of ".concat(t," ").concat(e.slice(0,n-1).join(", "),", or ")+e[n-1]:2===n?"one of ".concat(t," ").concat(e[0]," or ").concat(e[1]):"of ".concat(t," ").concat(e[0])}return"of ".concat(t," ").concat(String(e))}l("ERR_AMBIGUOUS_ARGUMENT",'The "%s" argument is ambiguous. %s',TypeError),l("ERR_INVALID_ARG_TYPE",function(e,t,r){var a,c,p,l,u;if(void 0===i&&(i=n(26093)),i("string"==typeof e,"'name' must be a string"),"string"==typeof t&&(c="not ",t.substr(0,4)===c)?(a="must not be",t=t.replace(/^not /,"")):a="must be",function(e,t,n){return(void 0===n||n>e.length)&&(n=e.length),e.substring(n-9,n)===t}(e," argument"))p="The ".concat(e," ").concat(a," ").concat(s(t,"type"));else{var b=("number"!=typeof u&&(u=0),u+1>(l=e).length||-1===l.indexOf(".",u)?"argument":"property");p='The "'.concat(e,'" ').concat(b," ").concat(a," ").concat(s(t,"type"))}return p+". Received type ".concat(o(r))},TypeError),l("ERR_INVALID_ARG_VALUE",function(e,t){var o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"is invalid";void 0===c&&(c=n(49208));var r=c.inspect(t);return r.length>128&&(r="".concat(r.slice(0,128),"...")),"The argument '".concat(e,"' ").concat(o,". Received ").concat(r)},TypeError,RangeError),l("ERR_INVALID_RETURN_VALUE",function(e,t,n){var r;return r=n&&n.constructor&&n.constructor.name?"instance of ".concat(n.constructor.name):"type ".concat(o(n)),"Expected ".concat(e,' to be returned from the "').concat(t,'"')+" function but got ".concat(r,".")},TypeError),l("ERR_MISSING_ARGS",function(){for(var e=arguments.length,t=new Array(e),o=0;o0,"At least one arg needs to be specified");var r="The ",a=t.length;switch(t=t.map(function(e){return'"'.concat(e,'"')}),a){case 1:r+="".concat(t[0]," argument");break;case 2:r+="".concat(t[0]," and ").concat(t[1]," arguments");break;default:r+=t.slice(0,a-1).join(", "),r+=", and ".concat(t[a-1]," arguments")}return"".concat(r," must be specified")},TypeError),e.exports.codes=p},5656:(e,t,n)=>{"use strict";function o(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var o,r,a,i,c=[],p=!0,l=!1;try{if(a=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;p=!1}else for(;!(p=(o=a.call(n)).done)&&(c.push(o.value),c.length!==t);p=!0);}catch(e){l=!0,r=e}finally{try{if(!p&&null!=n.return&&(i=n.return(),Object(i)!==i))return}finally{if(l)throw r}}return c}}(e,t)||function(e,t){if(e){if("string"==typeof e)return r(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?r(e,t):void 0}}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function r(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=new Array(t);n10)return!0;for(var t=0;t57)return!0}return 10===e.length&&e>=Math.pow(2,32)}function T(e){return Object.keys(e).filter(k).concat(s(e).filter(Object.prototype.propertyIsEnumerable.bind(e)))}function N(e,t){if(e===t)return 0;for(var n=e.length,o=t.length,r=0,a=Math.min(n,o);r{"use strict";var o=n(79138),r=n(6095),a=n(64531),i=n(87196);e.exports=i||o.call(a,r)},62012:(e,t,n)=>{"use strict";var o=n(79138),r=n(6095),a=n(68165);e.exports=function(){return a(o,r,arguments)}},6095:e=>{"use strict";e.exports=Function.prototype.apply},64531:e=>{"use strict";e.exports=Function.prototype.call},79903:(e,t,n)=>{"use strict";var o=n(79138),r=n(3468),a=n(64531),i=n(68165);e.exports=function(e){if(e.length<1||"function"!=typeof e[0])throw new r("a function is required");return i(o,a,e)}},87196:e=>{"use strict";e.exports="undefined"!=typeof Reflect&&Reflect&&Reflect.apply},79818:(e,t,n)=>{"use strict";var o=n(528),r=n(28498),a=r(o("String.prototype.indexOf"));e.exports=function(e,t){var n=o(e,!!t);return"function"==typeof n&&a(e,".prototype.")>-1?r(n):n}},28498:(e,t,n)=>{"use strict";var o=n(26108),r=n(64940),a=n(79903),i=n(62012);e.exports=function(e){var t=a(arguments),n=e.length-(arguments.length-1);return o(t,1+(n>0?n:0),!0)},r?r(e.exports,"apply",{value:i}):e.exports.apply=i},14607:(e,t,n)=>{"use strict";var o=n(528),r=n(79903),a=r([o("%String.prototype.indexOf%")]);e.exports=function(e,t){var n=o(e,!!t);return"function"==typeof n&&a(e,".prototype.")>-1?r([n]):n}},4364:(e,t,n)=>{var o=n(49208),r=n(26093);function a(){return(new Date).getTime()}var i,c=Array.prototype.slice,p={};i=void 0!==n.g&&n.g.console?n.g.console:"undefined"!=typeof window&&window.console?window.console:{};for(var l=[[function(){},"log"],[function(){i.log.apply(i,arguments)},"info"],[function(){i.log.apply(i,arguments)},"warn"],[function(){i.warn.apply(i,arguments)},"error"],[function(e){p[e]=a()},"time"],[function(e){var t=p[e];if(!t)throw new Error("No such label: "+e);delete p[e];var n=a()-t;i.log(e+": "+n+"ms")},"timeEnd"],[function(){var e=new Error;e.name="Trace",e.message=o.format.apply(null,arguments),i.error(e.stack)},"trace"],[function(e){i.log(o.inspect(e)+"\n")},"dir"],[function(e){if(!e){var t=c.call(arguments,1);r.ok(!1,o.format.apply(null,t))}},"assert"]],s=0;s{"use strict";n.d(t,{A:()=>O});var o=n(36758),r=n.n(o),a=n(40935),i=n.n(a),c=n(20062),p=n.n(c),l=new URL(n(2582),n.b),s=new URL(n(82143),n.b),u=new URL(n(64242),n.b),b=i()(r()),M=p()(l),d=p()(s),z=p()(u);b.push([e.id,"/* required styles */\r\n\r\n.leaflet-pane,\r\n.leaflet-tile,\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow,\r\n.leaflet-tile-container,\r\n.leaflet-pane > svg,\r\n.leaflet-pane > canvas,\r\n.leaflet-zoom-box,\r\n.leaflet-image-layer,\r\n.leaflet-layer {\r\n\tposition: absolute;\r\n\tleft: 0;\r\n\ttop: 0;\r\n\t}\r\n.leaflet-container {\r\n\toverflow: hidden;\r\n\t}\r\n.leaflet-tile,\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow {\r\n\t-webkit-user-select: none;\r\n\t -moz-user-select: none;\r\n\t user-select: none;\r\n\t -webkit-user-drag: none;\r\n\t}\r\n/* Prevents IE11 from highlighting tiles in blue */\r\n.leaflet-tile::selection {\r\n\tbackground: transparent;\r\n}\r\n/* Safari renders non-retina tile on retina better with this, but Chrome is worse */\r\n.leaflet-safari .leaflet-tile {\r\n\timage-rendering: -webkit-optimize-contrast;\r\n\t}\r\n/* hack that prevents hw layers \"stretching\" when loading new tiles */\r\n.leaflet-safari .leaflet-tile-container {\r\n\twidth: 1600px;\r\n\theight: 1600px;\r\n\t-webkit-transform-origin: 0 0;\r\n\t}\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow {\r\n\tdisplay: block;\r\n\t}\r\n/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */\r\n/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */\r\n.leaflet-container .leaflet-overlay-pane svg {\r\n\tmax-width: none !important;\r\n\tmax-height: none !important;\r\n\t}\r\n.leaflet-container .leaflet-marker-pane img,\r\n.leaflet-container .leaflet-shadow-pane img,\r\n.leaflet-container .leaflet-tile-pane img,\r\n.leaflet-container img.leaflet-image-layer,\r\n.leaflet-container .leaflet-tile {\r\n\tmax-width: none !important;\r\n\tmax-height: none !important;\r\n\twidth: auto;\r\n\tpadding: 0;\r\n\t}\r\n\r\n.leaflet-container img.leaflet-tile {\r\n\t/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */\r\n\tmix-blend-mode: plus-lighter;\r\n}\r\n\r\n.leaflet-container.leaflet-touch-zoom {\r\n\t-ms-touch-action: pan-x pan-y;\r\n\ttouch-action: pan-x pan-y;\r\n\t}\r\n.leaflet-container.leaflet-touch-drag {\r\n\t-ms-touch-action: pinch-zoom;\r\n\t/* Fallback for FF which doesn't support pinch-zoom */\r\n\ttouch-action: none;\r\n\ttouch-action: pinch-zoom;\r\n}\r\n.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {\r\n\t-ms-touch-action: none;\r\n\ttouch-action: none;\r\n}\r\n.leaflet-container {\r\n\t-webkit-tap-highlight-color: transparent;\r\n}\r\n.leaflet-container a {\r\n\t-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);\r\n}\r\n.leaflet-tile {\r\n\tfilter: inherit;\r\n\tvisibility: hidden;\r\n\t}\r\n.leaflet-tile-loaded {\r\n\tvisibility: inherit;\r\n\t}\r\n.leaflet-zoom-box {\r\n\twidth: 0;\r\n\theight: 0;\r\n\t-moz-box-sizing: border-box;\r\n\t box-sizing: border-box;\r\n\tz-index: 800;\r\n\t}\r\n/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */\r\n.leaflet-overlay-pane svg {\r\n\t-moz-user-select: none;\r\n\t}\r\n\r\n.leaflet-pane { z-index: 400; }\r\n\r\n.leaflet-tile-pane { z-index: 200; }\r\n.leaflet-overlay-pane { z-index: 400; }\r\n.leaflet-shadow-pane { z-index: 500; }\r\n.leaflet-marker-pane { z-index: 600; }\r\n.leaflet-tooltip-pane { z-index: 650; }\r\n.leaflet-popup-pane { z-index: 700; }\r\n\r\n.leaflet-map-pane canvas { z-index: 100; }\r\n.leaflet-map-pane svg { z-index: 200; }\r\n\r\n.leaflet-vml-shape {\r\n\twidth: 1px;\r\n\theight: 1px;\r\n\t}\r\n.lvml {\r\n\tbehavior: url(#default#VML);\r\n\tdisplay: inline-block;\r\n\tposition: absolute;\r\n\t}\r\n\r\n\r\n/* control positioning */\r\n\r\n.leaflet-control {\r\n\tposition: relative;\r\n\tz-index: 800;\r\n\tpointer-events: visiblePainted; /* IE 9-10 doesn't have auto */\r\n\tpointer-events: auto;\r\n\t}\r\n.leaflet-top,\r\n.leaflet-bottom {\r\n\tposition: absolute;\r\n\tz-index: 1000;\r\n\tpointer-events: none;\r\n\t}\r\n.leaflet-top {\r\n\ttop: 0;\r\n\t}\r\n.leaflet-right {\r\n\tright: 0;\r\n\t}\r\n.leaflet-bottom {\r\n\tbottom: 0;\r\n\t}\r\n.leaflet-left {\r\n\tleft: 0;\r\n\t}\r\n.leaflet-control {\r\n\tfloat: left;\r\n\tclear: both;\r\n\t}\r\n.leaflet-right .leaflet-control {\r\n\tfloat: right;\r\n\t}\r\n.leaflet-top .leaflet-control {\r\n\tmargin-top: 10px;\r\n\t}\r\n.leaflet-bottom .leaflet-control {\r\n\tmargin-bottom: 10px;\r\n\t}\r\n.leaflet-left .leaflet-control {\r\n\tmargin-left: 10px;\r\n\t}\r\n.leaflet-right .leaflet-control {\r\n\tmargin-right: 10px;\r\n\t}\r\n\r\n\r\n/* zoom and fade animations */\r\n\r\n.leaflet-fade-anim .leaflet-popup {\r\n\topacity: 0;\r\n\t-webkit-transition: opacity 0.2s linear;\r\n\t -moz-transition: opacity 0.2s linear;\r\n\t transition: opacity 0.2s linear;\r\n\t}\r\n.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {\r\n\topacity: 1;\r\n\t}\r\n.leaflet-zoom-animated {\r\n\t-webkit-transform-origin: 0 0;\r\n\t -ms-transform-origin: 0 0;\r\n\t transform-origin: 0 0;\r\n\t}\r\nsvg.leaflet-zoom-animated {\r\n\twill-change: transform;\r\n}\r\n\r\n.leaflet-zoom-anim .leaflet-zoom-animated {\r\n\t-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);\r\n\t -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);\r\n\t transition: transform 0.25s cubic-bezier(0,0,0.25,1);\r\n\t}\r\n.leaflet-zoom-anim .leaflet-tile,\r\n.leaflet-pan-anim .leaflet-tile {\r\n\t-webkit-transition: none;\r\n\t -moz-transition: none;\r\n\t transition: none;\r\n\t}\r\n\r\n.leaflet-zoom-anim .leaflet-zoom-hide {\r\n\tvisibility: hidden;\r\n\t}\r\n\r\n\r\n/* cursors */\r\n\r\n.leaflet-interactive {\r\n\tcursor: pointer;\r\n\t}\r\n.leaflet-grab {\r\n\tcursor: -webkit-grab;\r\n\tcursor: -moz-grab;\r\n\tcursor: grab;\r\n\t}\r\n.leaflet-crosshair,\r\n.leaflet-crosshair .leaflet-interactive {\r\n\tcursor: crosshair;\r\n\t}\r\n.leaflet-popup-pane,\r\n.leaflet-control {\r\n\tcursor: auto;\r\n\t}\r\n.leaflet-dragging .leaflet-grab,\r\n.leaflet-dragging .leaflet-grab .leaflet-interactive,\r\n.leaflet-dragging .leaflet-marker-draggable {\r\n\tcursor: move;\r\n\tcursor: -webkit-grabbing;\r\n\tcursor: -moz-grabbing;\r\n\tcursor: grabbing;\r\n\t}\r\n\r\n/* marker & overlays interactivity */\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow,\r\n.leaflet-image-layer,\r\n.leaflet-pane > svg path,\r\n.leaflet-tile-container {\r\n\tpointer-events: none;\r\n\t}\r\n\r\n.leaflet-marker-icon.leaflet-interactive,\r\n.leaflet-image-layer.leaflet-interactive,\r\n.leaflet-pane > svg path.leaflet-interactive,\r\nsvg.leaflet-image-layer.leaflet-interactive path {\r\n\tpointer-events: visiblePainted; /* IE 9-10 doesn't have auto */\r\n\tpointer-events: auto;\r\n\t}\r\n\r\n/* visual tweaks */\r\n\r\n.leaflet-container {\r\n\tbackground: #ddd;\r\n\toutline-offset: 1px;\r\n\t}\r\n.leaflet-container a {\r\n\tcolor: #0078A8;\r\n\t}\r\n.leaflet-zoom-box {\r\n\tborder: 2px dotted #38f;\r\n\tbackground: rgba(255,255,255,0.5);\r\n\t}\r\n\r\n\r\n/* general typography */\r\n.leaflet-container {\r\n\tfont-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\r\n\tfont-size: 12px;\r\n\tfont-size: 0.75rem;\r\n\tline-height: 1.5;\r\n\t}\r\n\r\n\r\n/* general toolbar styles */\r\n\r\n.leaflet-bar {\r\n\tbox-shadow: 0 1px 5px rgba(0,0,0,0.65);\r\n\tborder-radius: 4px;\r\n\t}\r\n.leaflet-bar a {\r\n\tbackground-color: #fff;\r\n\tborder-bottom: 1px solid #ccc;\r\n\twidth: 26px;\r\n\theight: 26px;\r\n\tline-height: 26px;\r\n\tdisplay: block;\r\n\ttext-align: center;\r\n\ttext-decoration: none;\r\n\tcolor: black;\r\n\t}\r\n.leaflet-bar a,\r\n.leaflet-control-layers-toggle {\r\n\tbackground-position: 50% 50%;\r\n\tbackground-repeat: no-repeat;\r\n\tdisplay: block;\r\n\t}\r\n.leaflet-bar a:hover,\r\n.leaflet-bar a:focus {\r\n\tbackground-color: #f4f4f4;\r\n\t}\r\n.leaflet-bar a:first-child {\r\n\tborder-top-left-radius: 4px;\r\n\tborder-top-right-radius: 4px;\r\n\t}\r\n.leaflet-bar a:last-child {\r\n\tborder-bottom-left-radius: 4px;\r\n\tborder-bottom-right-radius: 4px;\r\n\tborder-bottom: none;\r\n\t}\r\n.leaflet-bar a.leaflet-disabled {\r\n\tcursor: default;\r\n\tbackground-color: #f4f4f4;\r\n\tcolor: #bbb;\r\n\t}\r\n\r\n.leaflet-touch .leaflet-bar a {\r\n\twidth: 30px;\r\n\theight: 30px;\r\n\tline-height: 30px;\r\n\t}\r\n.leaflet-touch .leaflet-bar a:first-child {\r\n\tborder-top-left-radius: 2px;\r\n\tborder-top-right-radius: 2px;\r\n\t}\r\n.leaflet-touch .leaflet-bar a:last-child {\r\n\tborder-bottom-left-radius: 2px;\r\n\tborder-bottom-right-radius: 2px;\r\n\t}\r\n\r\n/* zoom control */\r\n\r\n.leaflet-control-zoom-in,\r\n.leaflet-control-zoom-out {\r\n\tfont: bold 18px 'Lucida Console', Monaco, monospace;\r\n\ttext-indent: 1px;\r\n\t}\r\n\r\n.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {\r\n\tfont-size: 22px;\r\n\t}\r\n\r\n\r\n/* layers control */\r\n\r\n.leaflet-control-layers {\r\n\tbox-shadow: 0 1px 5px rgba(0,0,0,0.4);\r\n\tbackground: #fff;\r\n\tborder-radius: 5px;\r\n\t}\r\n.leaflet-control-layers-toggle {\r\n\tbackground-image: url("+M+");\r\n\twidth: 36px;\r\n\theight: 36px;\r\n\t}\r\n.leaflet-retina .leaflet-control-layers-toggle {\r\n\tbackground-image: url("+d+");\r\n\tbackground-size: 26px 26px;\r\n\t}\r\n.leaflet-touch .leaflet-control-layers-toggle {\r\n\twidth: 44px;\r\n\theight: 44px;\r\n\t}\r\n.leaflet-control-layers .leaflet-control-layers-list,\r\n.leaflet-control-layers-expanded .leaflet-control-layers-toggle {\r\n\tdisplay: none;\r\n\t}\r\n.leaflet-control-layers-expanded .leaflet-control-layers-list {\r\n\tdisplay: block;\r\n\tposition: relative;\r\n\t}\r\n.leaflet-control-layers-expanded {\r\n\tpadding: 6px 10px 6px 6px;\r\n\tcolor: #333;\r\n\tbackground: #fff;\r\n\t}\r\n.leaflet-control-layers-scrollbar {\r\n\toverflow-y: scroll;\r\n\toverflow-x: hidden;\r\n\tpadding-right: 5px;\r\n\t}\r\n.leaflet-control-layers-selector {\r\n\tmargin-top: 2px;\r\n\tposition: relative;\r\n\ttop: 1px;\r\n\t}\r\n.leaflet-control-layers label {\r\n\tdisplay: block;\r\n\tfont-size: 13px;\r\n\tfont-size: 1.08333em;\r\n\t}\r\n.leaflet-control-layers-separator {\r\n\theight: 0;\r\n\tborder-top: 1px solid #ddd;\r\n\tmargin: 5px -10px 5px -6px;\r\n\t}\r\n\r\n/* Default icon URLs */\r\n.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */\r\n\tbackground-image: url("+z+');\r\n\t}\r\n\r\n\r\n/* attribution and scale controls */\r\n\r\n.leaflet-container .leaflet-control-attribution {\r\n\tbackground: #fff;\r\n\tbackground: rgba(255, 255, 255, 0.8);\r\n\tmargin: 0;\r\n\t}\r\n.leaflet-control-attribution,\r\n.leaflet-control-scale-line {\r\n\tpadding: 0 5px;\r\n\tcolor: #333;\r\n\tline-height: 1.4;\r\n\t}\r\n.leaflet-control-attribution a {\r\n\ttext-decoration: none;\r\n\t}\r\n.leaflet-control-attribution a:hover,\r\n.leaflet-control-attribution a:focus {\r\n\ttext-decoration: underline;\r\n\t}\r\n.leaflet-attribution-flag {\r\n\tdisplay: inline !important;\r\n\tvertical-align: baseline !important;\r\n\twidth: 1em;\r\n\theight: 0.6669em;\r\n\t}\r\n.leaflet-left .leaflet-control-scale {\r\n\tmargin-left: 5px;\r\n\t}\r\n.leaflet-bottom .leaflet-control-scale {\r\n\tmargin-bottom: 5px;\r\n\t}\r\n.leaflet-control-scale-line {\r\n\tborder: 2px solid #777;\r\n\tborder-top: none;\r\n\tline-height: 1.1;\r\n\tpadding: 2px 5px 1px;\r\n\twhite-space: nowrap;\r\n\t-moz-box-sizing: border-box;\r\n\t box-sizing: border-box;\r\n\tbackground: rgba(255, 255, 255, 0.8);\r\n\ttext-shadow: 1px 1px #fff;\r\n\t}\r\n.leaflet-control-scale-line:not(:first-child) {\r\n\tborder-top: 2px solid #777;\r\n\tborder-bottom: none;\r\n\tmargin-top: -2px;\r\n\t}\r\n.leaflet-control-scale-line:not(:first-child):not(:last-child) {\r\n\tborder-bottom: 2px solid #777;\r\n\t}\r\n\r\n.leaflet-touch .leaflet-control-attribution,\r\n.leaflet-touch .leaflet-control-layers,\r\n.leaflet-touch .leaflet-bar {\r\n\tbox-shadow: none;\r\n\t}\r\n.leaflet-touch .leaflet-control-layers,\r\n.leaflet-touch .leaflet-bar {\r\n\tborder: 2px solid rgba(0,0,0,0.2);\r\n\tbackground-clip: padding-box;\r\n\t}\r\n\r\n\r\n/* popup */\r\n\r\n.leaflet-popup {\r\n\tposition: absolute;\r\n\ttext-align: center;\r\n\tmargin-bottom: 20px;\r\n\t}\r\n.leaflet-popup-content-wrapper {\r\n\tpadding: 1px;\r\n\ttext-align: left;\r\n\tborder-radius: 12px;\r\n\t}\r\n.leaflet-popup-content {\r\n\tmargin: 13px 24px 13px 20px;\r\n\tline-height: 1.3;\r\n\tfont-size: 13px;\r\n\tfont-size: 1.08333em;\r\n\tmin-height: 1px;\r\n\t}\r\n.leaflet-popup-content p {\r\n\tmargin: 17px 0;\r\n\tmargin: 1.3em 0;\r\n\t}\r\n.leaflet-popup-tip-container {\r\n\twidth: 40px;\r\n\theight: 20px;\r\n\tposition: absolute;\r\n\tleft: 50%;\r\n\tmargin-top: -1px;\r\n\tmargin-left: -20px;\r\n\toverflow: hidden;\r\n\tpointer-events: none;\r\n\t}\r\n.leaflet-popup-tip {\r\n\twidth: 17px;\r\n\theight: 17px;\r\n\tpadding: 1px;\r\n\r\n\tmargin: -10px auto 0;\r\n\tpointer-events: auto;\r\n\r\n\t-webkit-transform: rotate(45deg);\r\n\t -moz-transform: rotate(45deg);\r\n\t -ms-transform: rotate(45deg);\r\n\t transform: rotate(45deg);\r\n\t}\r\n.leaflet-popup-content-wrapper,\r\n.leaflet-popup-tip {\r\n\tbackground: white;\r\n\tcolor: #333;\r\n\tbox-shadow: 0 3px 14px rgba(0,0,0,0.4);\r\n\t}\r\n.leaflet-container a.leaflet-popup-close-button {\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tright: 0;\r\n\tborder: none;\r\n\ttext-align: center;\r\n\twidth: 24px;\r\n\theight: 24px;\r\n\tfont: 16px/24px Tahoma, Verdana, sans-serif;\r\n\tcolor: #757575;\r\n\ttext-decoration: none;\r\n\tbackground: transparent;\r\n\t}\r\n.leaflet-container a.leaflet-popup-close-button:hover,\r\n.leaflet-container a.leaflet-popup-close-button:focus {\r\n\tcolor: #585858;\r\n\t}\r\n.leaflet-popup-scrolled {\r\n\toverflow: auto;\r\n\t}\r\n\r\n.leaflet-oldie .leaflet-popup-content-wrapper {\r\n\t-ms-zoom: 1;\r\n\t}\r\n.leaflet-oldie .leaflet-popup-tip {\r\n\twidth: 24px;\r\n\tmargin: 0 auto;\r\n\r\n\t-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";\r\n\tfilter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);\r\n\t}\r\n\r\n.leaflet-oldie .leaflet-control-zoom,\r\n.leaflet-oldie .leaflet-control-layers,\r\n.leaflet-oldie .leaflet-popup-content-wrapper,\r\n.leaflet-oldie .leaflet-popup-tip {\r\n\tborder: 1px solid #999;\r\n\t}\r\n\r\n\r\n/* div icon */\r\n\r\n.leaflet-div-icon {\r\n\tbackground: #fff;\r\n\tborder: 1px solid #666;\r\n\t}\r\n\r\n\r\n/* Tooltip */\r\n/* Base styles for the element that has a tooltip */\r\n.leaflet-tooltip {\r\n\tposition: absolute;\r\n\tpadding: 6px;\r\n\tbackground-color: #fff;\r\n\tborder: 1px solid #fff;\r\n\tborder-radius: 3px;\r\n\tcolor: #222;\r\n\twhite-space: nowrap;\r\n\t-webkit-user-select: none;\r\n\t-moz-user-select: none;\r\n\t-ms-user-select: none;\r\n\tuser-select: none;\r\n\tpointer-events: none;\r\n\tbox-shadow: 0 1px 3px rgba(0,0,0,0.4);\r\n\t}\r\n.leaflet-tooltip.leaflet-interactive {\r\n\tcursor: pointer;\r\n\tpointer-events: auto;\r\n\t}\r\n.leaflet-tooltip-top:before,\r\n.leaflet-tooltip-bottom:before,\r\n.leaflet-tooltip-left:before,\r\n.leaflet-tooltip-right:before {\r\n\tposition: absolute;\r\n\tpointer-events: none;\r\n\tborder: 6px solid transparent;\r\n\tbackground: transparent;\r\n\tcontent: "";\r\n\t}\r\n\r\n/* Directions */\r\n\r\n.leaflet-tooltip-bottom {\r\n\tmargin-top: 6px;\r\n}\r\n.leaflet-tooltip-top {\r\n\tmargin-top: -6px;\r\n}\r\n.leaflet-tooltip-bottom:before,\r\n.leaflet-tooltip-top:before {\r\n\tleft: 50%;\r\n\tmargin-left: -6px;\r\n\t}\r\n.leaflet-tooltip-top:before {\r\n\tbottom: 0;\r\n\tmargin-bottom: -12px;\r\n\tborder-top-color: #fff;\r\n\t}\r\n.leaflet-tooltip-bottom:before {\r\n\ttop: 0;\r\n\tmargin-top: -12px;\r\n\tmargin-left: -6px;\r\n\tborder-bottom-color: #fff;\r\n\t}\r\n.leaflet-tooltip-left {\r\n\tmargin-left: -6px;\r\n}\r\n.leaflet-tooltip-right {\r\n\tmargin-left: 6px;\r\n}\r\n.leaflet-tooltip-left:before,\r\n.leaflet-tooltip-right:before {\r\n\ttop: 50%;\r\n\tmargin-top: -6px;\r\n\t}\r\n.leaflet-tooltip-left:before {\r\n\tright: 0;\r\n\tmargin-right: -12px;\r\n\tborder-left-color: #fff;\r\n\t}\r\n.leaflet-tooltip-right:before {\r\n\tleft: 0;\r\n\tmargin-left: -12px;\r\n\tborder-right-color: #fff;\r\n\t}\r\n\r\n/* Printing */\r\n\r\n@media print {\r\n\t/* Prevent printers from removing background-images of controls. */\r\n\t.leaflet-control {\r\n\t\t-webkit-print-color-adjust: exact;\r\n\t\tprint-color-adjust: exact;\r\n\t\t}\r\n\t}\r\n',""]);const O=b},40935:e=>{"use strict";e.exports=function(e){var t=[];return t.toString=function(){return this.map(function(t){var n="",o=void 0!==t[5];return t[4]&&(n+="@supports (".concat(t[4],") {")),t[2]&&(n+="@media ".concat(t[2]," {")),o&&(n+="@layer".concat(t[5].length>0?" ".concat(t[5]):""," {")),n+=e(t),o&&(n+="}"),t[2]&&(n+="}"),t[4]&&(n+="}"),n}).join("")},t.i=function(e,n,o,r,a){"string"==typeof e&&(e=[[null,e,void 0]]);var i={};if(o)for(var c=0;c0?" ".concat(s[5]):""," {").concat(s[1],"}")),s[5]=a),n&&(s[2]?(s[1]="@media ".concat(s[2]," {").concat(s[1],"}"),s[2]=n):s[2]=n),r&&(s[4]?(s[1]="@supports (".concat(s[4],") {").concat(s[1],"}"),s[4]=r):s[4]="".concat(r)),t.push(s))}},t}},20062:e=>{"use strict";e.exports=function(e,t){return t||(t={}),e?(e=String(e.__esModule?e.default:e),/^['"].*['"]$/.test(e)&&(e=e.slice(1,-1)),t.hash&&(e+=t.hash),/["'() \t\n]|(%20)/.test(e)||t.needQuotes?'"'.concat(e.replace(/"/g,'\\"').replace(/\n/g,"\\n"),'"'):e):e}},36758:e=>{"use strict";e.exports=function(e){return e[1]}},70686:(e,t,n)=>{"use strict";var o=n(64940),r=n(5731),a=n(3468),i=n(69336);e.exports=function(e,t,n){if(!e||"object"!=typeof e&&"function"!=typeof e)throw new a("`obj` must be an object or a function`");if("string"!=typeof t&&"symbol"!=typeof t)throw new a("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new a("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new a("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new a("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new a("`loose`, if provided, must be a boolean");var c=arguments.length>3?arguments[3]:null,p=arguments.length>4?arguments[4]:null,l=arguments.length>5?arguments[5]:null,s=arguments.length>6&&arguments[6],u=!!i&&i(e,t);if(o)o(e,t,{configurable:null===l&&u?u.configurable:!l,enumerable:null===c&&u?u.enumerable:!c,value:n,writable:null===p&&u?u.writable:!p});else{if(!s&&(c||p||l))throw new r("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");e[t]=n}}},41857:(e,t,n)=>{"use strict";var o=n(49228),r="function"==typeof Symbol&&"symbol"==typeof Symbol("foo"),a=Object.prototype.toString,i=Array.prototype.concat,c=n(70686),p=n(17239)(),l=function(e,t,n,o){if(t in e)if(!0===o){if(e[t]===n)return}else if("function"!=typeof(r=o)||"[object Function]"!==a.call(r)||!o())return;var r;p?c(e,t,n,!0):c(e,t,n)},s=function(e,t){var n=arguments.length>2?arguments[2]:{},a=o(t);r&&(a=i.call(a,Object.getOwnPropertySymbols(t)));for(var c=0;c{"use strict";var o,r=n(79903),a=n(69336);try{o=[].__proto__===Array.prototype}catch(e){if(!e||"object"!=typeof e||!("code"in e)||"ERR_PROTO_ACCESS"!==e.code)throw e}var i=!!o&&a&&a(Object.prototype,"__proto__"),c=Object,p=c.getPrototypeOf;e.exports=i&&"function"==typeof i.get?r([i.get]):"function"==typeof p&&function(e){return p(null==e?e:c(e))}},64940:e=>{"use strict";var t=Object.defineProperty||!1;if(t)try{t({},"a",{value:1})}catch(e){t=!1}e.exports=t},29110:e=>{"use strict";e.exports=EvalError},29838:e=>{"use strict";e.exports=Error},61155:e=>{"use strict";e.exports=RangeError},94943:e=>{"use strict";e.exports=ReferenceError},5731:e=>{"use strict";e.exports=SyntaxError},3468:e=>{"use strict";e.exports=TypeError},32140:e=>{"use strict";e.exports=URIError},9629:e=>{"use strict";e.exports=Object},80705:(e,t,n)=>{"use strict";var o=n(89617),r=Object.prototype.toString,a=Object.prototype.hasOwnProperty;e.exports=function(e,t,n){if(!o(t))throw new TypeError("iterator must be a function");var i,c;arguments.length>=3&&(i=n),c=e,"[object Array]"===r.call(c)?function(e,t,n){for(var o=0,r=e.length;o{"use strict";var t=Object.prototype.toString,n=Math.max,o=function(e,t){for(var n=[],o=0;o{"use strict";var o=n(28794);e.exports=Function.prototype.bind||o},14898:e=>{"use strict";const t=function*(){}.constructor;e.exports=()=>t},528:(e,t,n)=>{"use strict";var o,r=n(9629),a=n(29838),i=n(29110),c=n(61155),p=n(94943),l=n(5731),s=n(3468),u=n(32140),b=n(58479),M=n(18449),d=n(88129),z=n(22387),O=n(85865),f=n(1319),h=n(36882),A=Function,m=function(e){try{return A('"use strict"; return ('+e+").constructor;")()}catch(e){}},q=n(69336),v=n(64940),_=function(){throw new s},y=q?function(){try{return _}catch(e){try{return q(arguments,"callee").get}catch(e){return _}}}():_,g=n(53558)(),W=n(46369),L=n(17345),R=n(57859),w=n(6095),S=n(64531),x={},k="undefined"!=typeof Uint8Array&&W?W(Uint8Array):o,T={__proto__:null,"%AggregateError%":"undefined"==typeof AggregateError?o:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?o:ArrayBuffer,"%ArrayIteratorPrototype%":g&&W?W([][Symbol.iterator]()):o,"%AsyncFromSyncIteratorPrototype%":o,"%AsyncFunction%":x,"%AsyncGenerator%":x,"%AsyncGeneratorFunction%":x,"%AsyncIteratorPrototype%":x,"%Atomics%":"undefined"==typeof Atomics?o:Atomics,"%BigInt%":"undefined"==typeof BigInt?o:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?o:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?o:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?o:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":a,"%eval%":eval,"%EvalError%":i,"%Float16Array%":"undefined"==typeof Float16Array?o:Float16Array,"%Float32Array%":"undefined"==typeof Float32Array?o:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?o:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?o:FinalizationRegistry,"%Function%":A,"%GeneratorFunction%":x,"%Int8Array%":"undefined"==typeof Int8Array?o:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?o:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?o:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":g&&W?W(W([][Symbol.iterator]())):o,"%JSON%":"object"==typeof JSON?JSON:o,"%Map%":"undefined"==typeof Map?o:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&g&&W?W((new Map)[Symbol.iterator]()):o,"%Math%":Math,"%Number%":Number,"%Object%":r,"%Object.getOwnPropertyDescriptor%":q,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?o:Promise,"%Proxy%":"undefined"==typeof Proxy?o:Proxy,"%RangeError%":c,"%ReferenceError%":p,"%Reflect%":"undefined"==typeof Reflect?o:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?o:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&g&&W?W((new Set)[Symbol.iterator]()):o,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?o:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":g&&W?W(""[Symbol.iterator]()):o,"%Symbol%":g?Symbol:o,"%SyntaxError%":l,"%ThrowTypeError%":y,"%TypedArray%":k,"%TypeError%":s,"%Uint8Array%":"undefined"==typeof Uint8Array?o:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?o:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?o:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?o:Uint32Array,"%URIError%":u,"%WeakMap%":"undefined"==typeof WeakMap?o:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?o:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?o:WeakSet,"%Function.prototype.call%":S,"%Function.prototype.apply%":w,"%Object.defineProperty%":v,"%Object.getPrototypeOf%":L,"%Math.abs%":b,"%Math.floor%":M,"%Math.max%":d,"%Math.min%":z,"%Math.pow%":O,"%Math.round%":f,"%Math.sign%":h,"%Reflect.getPrototypeOf%":R};if(W)try{null.error}catch(e){var N=W(W(e));T["%Error.prototype%"]=N}var E=function e(t){var n;if("%AsyncFunction%"===t)n=m("async function () {}");else if("%GeneratorFunction%"===t)n=m("function* () {}");else if("%AsyncGeneratorFunction%"===t)n=m("async function* () {}");else if("%AsyncGenerator%"===t){var o=e("%AsyncGeneratorFunction%");o&&(n=o.prototype)}else if("%AsyncIteratorPrototype%"===t){var r=e("%AsyncGenerator%");r&&W&&(n=W(r.prototype))}return T[t]=n,n},B={__proto__:null,"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},D=n(79138),P=n(78554),C=D.call(S,Array.prototype.concat),j=D.call(w,Array.prototype.splice),Y=D.call(S,String.prototype.replace),X=D.call(S,String.prototype.slice),F=D.call(S,RegExp.prototype.exec),H=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,I=/\\(\\)?/g,U=function(e,t){var n,o=e;if(P(B,o)&&(o="%"+(n=B[o])[0]+"%"),P(T,o)){var r=T[o];if(r===x&&(r=E(o)),void 0===r&&!t)throw new s("intrinsic "+e+" exists, but is not available. Please file an issue!");return{alias:n,name:o,value:r}}throw new l("intrinsic "+e+" does not exist!")};e.exports=function(e,t){if("string"!=typeof e||0===e.length)throw new s("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof t)throw new s('"allowMissing" argument must be a boolean');if(null===F(/^%?[^%]*%?$/,e))throw new l("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var n=function(e){var t=X(e,0,1),n=X(e,-1);if("%"===t&&"%"!==n)throw new l("invalid intrinsic syntax, expected closing `%`");if("%"===n&&"%"!==t)throw new l("invalid intrinsic syntax, expected opening `%`");var o=[];return Y(e,H,function(e,t,n,r){o[o.length]=n?Y(r,I,"$1"):t||e}),o}(e),o=n.length>0?n[0]:"",r=U("%"+o+"%",t),a=r.name,i=r.value,c=!1,p=r.alias;p&&(o=p[0],j(n,C([0,1],p)));for(var u=1,b=!0;u=n.length){var O=q(i,M);i=(b=!!O)&&"get"in O&&!("originalValue"in O.get)?O.get:i[M]}else b=P(i,M),i=i[M];b&&!c&&(T[a]=i)}}return i}},17345:(e,t,n)=>{"use strict";var o=n(9629);e.exports=o.getPrototypeOf||null},57859:e=>{"use strict";e.exports="undefined"!=typeof Reflect&&Reflect.getPrototypeOf||null},46369:(e,t,n)=>{"use strict";var o=n(57859),r=n(17345),a=n(46423);e.exports=o?function(e){return o(e)}:r?function(e){if(!e||"object"!=typeof e&&"function"!=typeof e)throw new TypeError("getProto: not an object");return r(e)}:a?function(e){return a(e)}:null},61292:e=>{"use strict";e.exports=Object.getOwnPropertyDescriptor},69336:(e,t,n)=>{"use strict";var o=n(61292);if(o)try{o([],"length")}catch(e){o=null}e.exports=o},17239:(e,t,n)=>{"use strict";var o=n(64940),r=function(){return!!o};r.hasArrayLengthDefineBug=function(){if(!o)return null;try{return 1!==o([],"length",{value:1}).length}catch(e){return!0}},e.exports=r},53558:(e,t,n)=>{"use strict";var o="undefined"!=typeof Symbol&&Symbol,r=n(62908);e.exports=function(){return"function"==typeof o&&"function"==typeof Symbol&&"symbol"==typeof o("foo")&&"symbol"==typeof Symbol("bar")&&r()}},62908:e=>{"use strict";e.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var e={},t=Symbol("test"),n=Object(t);if("string"==typeof t)return!1;if("[object Symbol]"!==Object.prototype.toString.call(t))return!1;if("[object Symbol]"!==Object.prototype.toString.call(n))return!1;for(var o in e[t]=42,e)return!1;if("function"==typeof Object.keys&&0!==Object.keys(e).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(e).length)return!1;var r=Object.getOwnPropertySymbols(e);if(1!==r.length||r[0]!==t)return!1;if(!Object.prototype.propertyIsEnumerable.call(e,t))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var a=Object.getOwnPropertyDescriptor(e,t);if(42!==a.value||!0!==a.enumerable)return!1}return!0}},51913:(e,t,n)=>{"use strict";var o=n(62908);e.exports=function(){return o()&&!!Symbol.toStringTag}},78554:(e,t,n)=>{"use strict";var o=Function.prototype.call,r=Object.prototype.hasOwnProperty,a=n(79138);e.exports=a.call(o,r)},75985:(e,t,n)=>{"use strict";var o=n(65521),r={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},a={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},i={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},c={};function p(e){return o.isMemo(e)?i:c[e.$$typeof]||r}c[o.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},c[o.Memo]=i;var l=Object.defineProperty,s=Object.getOwnPropertyNames,u=Object.getOwnPropertySymbols,b=Object.getOwnPropertyDescriptor,M=Object.getPrototypeOf,d=Object.prototype;e.exports=function e(t,n,o){if("string"!=typeof n){if(d){var r=M(n);r&&r!==d&&e(t,r,o)}var i=s(n);u&&(i=i.concat(u(n)));for(var c=p(t),z=p(n),O=0;O{"use strict";var n="function"==typeof Symbol&&Symbol.for,o=n?Symbol.for("react.element"):60103,r=n?Symbol.for("react.portal"):60106,a=n?Symbol.for("react.fragment"):60107,i=n?Symbol.for("react.strict_mode"):60108,c=n?Symbol.for("react.profiler"):60114,p=n?Symbol.for("react.provider"):60109,l=n?Symbol.for("react.context"):60110,s=n?Symbol.for("react.async_mode"):60111,u=n?Symbol.for("react.concurrent_mode"):60111,b=n?Symbol.for("react.forward_ref"):60112,M=n?Symbol.for("react.suspense"):60113,d=n?Symbol.for("react.suspense_list"):60120,z=n?Symbol.for("react.memo"):60115,O=n?Symbol.for("react.lazy"):60116,f=n?Symbol.for("react.block"):60121,h=n?Symbol.for("react.fundamental"):60117,A=n?Symbol.for("react.responder"):60118,m=n?Symbol.for("react.scope"):60119;function q(e){if("object"==typeof e&&null!==e){var t=e.$$typeof;switch(t){case o:switch(e=e.type){case s:case u:case a:case c:case i:case M:return e;default:switch(e=e&&e.$$typeof){case l:case b:case O:case z:case p:return e;default:return t}}case r:return t}}}function v(e){return q(e)===u}t.AsyncMode=s,t.ConcurrentMode=u,t.ContextConsumer=l,t.ContextProvider=p,t.Element=o,t.ForwardRef=b,t.Fragment=a,t.Lazy=O,t.Memo=z,t.Portal=r,t.Profiler=c,t.StrictMode=i,t.Suspense=M,t.isAsyncMode=function(e){return v(e)||q(e)===s},t.isConcurrentMode=v,t.isContextConsumer=function(e){return q(e)===l},t.isContextProvider=function(e){return q(e)===p},t.isElement=function(e){return"object"==typeof e&&null!==e&&e.$$typeof===o},t.isForwardRef=function(e){return q(e)===b},t.isFragment=function(e){return q(e)===a},t.isLazy=function(e){return q(e)===O},t.isMemo=function(e){return q(e)===z},t.isPortal=function(e){return q(e)===r},t.isProfiler=function(e){return q(e)===c},t.isStrictMode=function(e){return q(e)===i},t.isSuspense=function(e){return q(e)===M},t.isValidElementType=function(e){return"string"==typeof e||"function"==typeof e||e===a||e===u||e===c||e===i||e===M||e===d||"object"==typeof e&&null!==e&&(e.$$typeof===O||e.$$typeof===z||e.$$typeof===p||e.$$typeof===l||e.$$typeof===b||e.$$typeof===h||e.$$typeof===A||e.$$typeof===m||e.$$typeof===f)},t.typeOf=q},65521:(e,t,n)=>{"use strict";e.exports=n(26685)},35615:e=>{"function"==typeof Object.create?e.exports=function(e,t){t&&(e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}))}:e.exports=function(e,t){if(t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}}},55387:(e,t,n)=>{"use strict";var o=n(51913)(),r=n(14607)("Object.prototype.toString"),a=function(e){return!(o&&e&&"object"==typeof e&&Symbol.toStringTag in e)&&"[object Arguments]"===r(e)},i=function(e){return!!a(e)||null!==e&&"object"==typeof e&&"length"in e&&"number"==typeof e.length&&e.length>=0&&"[object Array]"!==r(e)&&"callee"in e&&"[object Function]"===r(e.callee)},c=function(){return a(arguments)}();a.isLegacyArguments=i,e.exports=c?a:i},89617:e=>{"use strict";var t,n,o=Function.prototype.toString,r="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof r&&"function"==typeof Object.defineProperty)try{t=Object.defineProperty({},"length",{get:function(){throw n}}),n={},r(function(){throw 42},null,t)}catch(e){e!==n&&(r=null)}else r=null;var a=/^\s*class\b/,i=function(e){try{var t=o.call(e);return a.test(t)}catch(e){return!1}},c=function(e){try{return!i(e)&&(o.call(e),!0)}catch(e){return!1}},p=Object.prototype.toString,l="function"==typeof Symbol&&!!Symbol.toStringTag,s=!(0 in[,]),u=function(){return!1};if("object"==typeof document){var b=document.all;p.call(b)===p.call(document.all)&&(u=function(e){if((s||!e)&&(void 0===e||"object"==typeof e))try{var t=p.call(e);return("[object HTMLAllCollection]"===t||"[object HTML document.all class]"===t||"[object HTMLCollection]"===t||"[object Object]"===t)&&null==e("")}catch(e){}return!1})}e.exports=r?function(e){if(u(e))return!0;if(!e)return!1;if("function"!=typeof e&&"object"!=typeof e)return!1;try{r(e,null,t)}catch(e){if(e!==n)return!1}return!i(e)&&c(e)}:function(e){if(u(e))return!0;if(!e)return!1;if("function"!=typeof e&&"object"!=typeof e)return!1;if(l)return c(e);if(i(e))return!1;var t=p.call(e);return!("[object Function]"!==t&&"[object GeneratorFunction]"!==t&&!/^\[object HTML/.test(t))&&c(e)}},2625:(e,t,n)=>{"use strict";var o=n(14607),r=n(46132)(/^\s*(?:function)?\*/),a=n(51913)(),i=n(46369),c=o("Object.prototype.toString"),p=o("Function.prototype.toString"),l=n(14898);e.exports=function(e){if("function"!=typeof e)return!1;if(r(p(e)))return!0;if(!a)return"[object GeneratorFunction]"===c(e);if(!i)return!1;var t=l();return t&&i(e)===t.prototype}},98006:e=>{"use strict";e.exports=function(e){return e!=e}},7838:(e,t,n)=>{"use strict";var o=n(28498),r=n(41857),a=n(98006),i=n(41591),c=n(61641),p=o(i(),Number);r(p,{getPolyfill:i,implementation:a,shim:c}),e.exports=p},41591:(e,t,n)=>{"use strict";var o=n(98006);e.exports=function(){return Number.isNaN&&Number.isNaN(NaN)&&!Number.isNaN("a")?Number.isNaN:o}},61641:(e,t,n)=>{"use strict";var o=n(41857),r=n(41591);e.exports=function(){var e=r();return o(Number,{isNaN:e},{isNaN:function(){return Number.isNaN!==e}}),e}},52672:(e,t,n)=>{"use strict";var o,r=n(14607),a=n(51913)(),i=n(78554),c=n(69336);if(a){var p=r("RegExp.prototype.exec"),l={},s=function(){throw l},u={toString:s,valueOf:s};"symbol"==typeof Symbol.toPrimitive&&(u[Symbol.toPrimitive]=s),o=function(e){if(!e||"object"!=typeof e)return!1;var t=c(e,"lastIndex");if(!t||!i(t,"value"))return!1;try{p(e,u)}catch(e){return e===l}}}else{var b=r("Object.prototype.toString");o=function(e){return!(!e||"object"!=typeof e&&"function"!=typeof e)&&"[object RegExp]"===b(e)}}e.exports=o},95943:(e,t,n)=>{"use strict";var o=n(52730);e.exports=function(e){return!!o(e)}},34651:function(e,t){var n;!function(t,n){"use strict";"object"==typeof e.exports?e.exports=t.document?n(t,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return n(e)}:n(t)}("undefined"!=typeof window?window:this,function(o,r){"use strict";var a=[],i=Object.getPrototypeOf,c=a.slice,p=a.flat?function(e){return a.flat.call(e)}:function(e){return a.concat.apply([],e)},l=a.push,s=a.indexOf,u={},b=u.toString,M=u.hasOwnProperty,d=M.toString,z=d.call(Object),O={},f=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},h=function(e){return null!=e&&e===e.window},A=o.document,m={type:!0,src:!0,nonce:!0,noModule:!0};function q(e,t,n){var o,r,a=(n=n||A).createElement("script");if(a.text=e,t)for(o in m)(r=t[o]||t.getAttribute&&t.getAttribute(o))&&a.setAttribute(o,r);n.head.appendChild(a).parentNode.removeChild(a)}function v(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?u[b.call(e)]||"object":typeof e}var _="3.7.1",y=/HTML$/i,g=function(e,t){return new g.fn.init(e,t)};function W(e){var t=!!e&&"length"in e&&e.length,n=v(e);return!f(e)&&!h(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}function L(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}g.fn=g.prototype={jquery:_,constructor:g,length:0,toArray:function(){return c.call(this)},get:function(e){return null==e?c.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=g.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return g.each(this,e)},map:function(e){return this.pushStack(g.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(c.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(g.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(g.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n+~]|"+x+")"+x+"*"),Y=new RegExp(x+"|>"),X=new RegExp(D),F=new RegExp("^"+T+"$"),H={ID:new RegExp("^#("+T+")"),CLASS:new RegExp("^\\.("+T+")"),TAG:new RegExp("^("+T+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+D),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+x+"*(even|odd|(([+-]|)(\\d*)n|)"+x+"*(?:([+-]|)"+x+"*(\\d+)|))"+x+"*\\)|)","i"),bool:new RegExp("^(?:"+W+")$","i"),needsContext:new RegExp("^"+x+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+x+"*((?:-\\d)?\\d*)"+x+"*\\)|)(?=[^-]|$)","i")},I=/^(?:input|select|textarea|button)$/i,U=/^h\d$/i,G=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,V=/[+~]/,K=new RegExp("\\\\[\\da-fA-F]{1,6}"+x+"?|\\\\([^\\r\\n\\f])","g"),Z=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},J=function(){pe()},Q=be(function(e){return!0===e.disabled&&L(e,"fieldset")},{dir:"parentNode",next:"legend"});try{z.apply(a=c.call(E.childNodes),E.childNodes),a[E.childNodes.length].nodeType}catch(e){z={apply:function(e,t){B.apply(e,c.call(t))},call:function(e){B.apply(e,c.call(arguments,1))}}}function $(e,t,n,o){var r,a,i,c,l,s,M,d=t&&t.ownerDocument,h=t?t.nodeType:9;if(n=n||[],"string"!=typeof e||!e||1!==h&&9!==h&&11!==h)return n;if(!o&&(pe(t),t=t||p,u)){if(11!==h&&(l=G.exec(e)))if(r=l[1]){if(9===h){if(!(i=t.getElementById(r)))return n;if(i.id===r)return z.call(n,i),n}else if(d&&(i=d.getElementById(r))&&$.contains(t,i)&&i.id===r)return z.call(n,i),n}else{if(l[2])return z.apply(n,t.getElementsByTagName(e)),n;if((r=l[3])&&t.getElementsByClassName)return z.apply(n,t.getElementsByClassName(r)),n}if(!(_[e+" "]||b&&b.test(e))){if(M=e,d=t,1===h&&(Y.test(e)||j.test(e))){for((d=V.test(e)&&ce(t.parentNode)||t)==t&&O.scope||((c=t.getAttribute("id"))?c=g.escapeSelector(c):t.setAttribute("id",c=f)),a=(s=se(e)).length;a--;)s[a]=(c?"#"+c:":scope")+" "+ue(s[a]);M=s.join(",")}try{return z.apply(n,d.querySelectorAll(M)),n}catch(t){_(e,!0)}finally{c===f&&t.removeAttribute("id")}}}return he(e.replace(k,"$1"),t,n,o)}function ee(){var e=[];return function n(o,r){return e.push(o+" ")>t.cacheLength&&delete n[e.shift()],n[o+" "]=r}}function te(e){return e[f]=!0,e}function ne(e){var t=p.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function oe(e){return function(t){return L(t,"input")&&t.type===e}}function re(e){return function(t){return(L(t,"input")||L(t,"button"))&&t.type===e}}function ae(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&Q(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function ie(e){return te(function(t){return t=+t,te(function(n,o){for(var r,a=e([],n.length,t),i=a.length;i--;)n[r=a[i]]&&(n[r]=!(o[r]=n[r]))})})}function ce(e){return e&&void 0!==e.getElementsByTagName&&e}function pe(e){var n,o=e?e.ownerDocument||e:E;return o!=p&&9===o.nodeType&&o.documentElement?(l=(p=o).documentElement,u=!g.isXMLDoc(p),d=l.matches||l.webkitMatchesSelector||l.msMatchesSelector,l.msMatchesSelector&&E!=p&&(n=p.defaultView)&&n.top!==n&&n.addEventListener("unload",J),O.getById=ne(function(e){return l.appendChild(e).id=g.expando,!p.getElementsByName||!p.getElementsByName(g.expando).length}),O.disconnectedMatch=ne(function(e){return d.call(e,"*")}),O.scope=ne(function(){return p.querySelectorAll(":scope")}),O.cssHas=ne(function(){try{return p.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),O.getById?(t.filter.ID=function(e){var t=e.replace(K,Z);return function(e){return e.getAttribute("id")===t}},t.find.ID=function(e,t){if(void 0!==t.getElementById&&u){var n=t.getElementById(e);return n?[n]:[]}}):(t.filter.ID=function(e){var t=e.replace(K,Z);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},t.find.ID=function(e,t){if(void 0!==t.getElementById&&u){var n,o,r,a=t.getElementById(e);if(a){if((n=a.getAttributeNode("id"))&&n.value===e)return[a];for(r=t.getElementsByName(e),o=0;a=r[o++];)if((n=a.getAttributeNode("id"))&&n.value===e)return[a]}return[]}}),t.find.TAG=function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},t.find.CLASS=function(e,t){if(void 0!==t.getElementsByClassName&&u)return t.getElementsByClassName(e)},b=[],ne(function(e){var t;l.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||b.push("\\["+x+"*(?:value|"+W+")"),e.querySelectorAll("[id~="+f+"-]").length||b.push("~="),e.querySelectorAll("a#"+f+"+*").length||b.push(".#.+[+~]"),e.querySelectorAll(":checked").length||b.push(":checked"),(t=p.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),l.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&b.push(":enabled",":disabled"),(t=p.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||b.push("\\["+x+"*name"+x+"*="+x+"*(?:''|\"\")")}),O.cssHas||b.push(":has"),b=b.length&&new RegExp(b.join("|")),y=function(e,t){if(e===t)return i=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!O.sortDetached&&t.compareDocumentPosition(e)===n?e===p||e.ownerDocument==E&&$.contains(E,e)?-1:t===p||t.ownerDocument==E&&$.contains(E,t)?1:r?s.call(r,e)-s.call(r,t):0:4&n?-1:1)},p):p}for(e in $.matches=function(e,t){return $(e,null,null,t)},$.matchesSelector=function(e,t){if(pe(e),u&&!_[t+" "]&&(!b||!b.test(t)))try{var n=d.call(e,t);if(n||O.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){_(t,!0)}return $(t,p,null,[e]).length>0},$.contains=function(e,t){return(e.ownerDocument||e)!=p&&pe(e),g.contains(e,t)},$.attr=function(e,n){(e.ownerDocument||e)!=p&&pe(e);var o=t.attrHandle[n.toLowerCase()],r=o&&M.call(t.attrHandle,n.toLowerCase())?o(e,n,!u):void 0;return void 0!==r?r:e.getAttribute(n)},$.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},g.uniqueSort=function(e){var t,n=[],o=0,a=0;if(i=!O.sortStable,r=!O.sortStable&&c.call(e,0),w.call(e,y),i){for(;t=e[a++];)t===e[a]&&(o=n.push(a));for(;o--;)S.call(e,n[o],1)}return r=null,e},g.fn.uniqueSort=function(){return this.pushStack(g.uniqueSort(c.apply(this)))},t=g.expr={cacheLength:50,createPseudo:te,match:H,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(K,Z),e[3]=(e[3]||e[4]||e[5]||"").replace(K,Z),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||$.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&$.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return H.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=se(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(K,Z).toLowerCase();return"*"===e?function(){return!0}:function(e){return L(e,t)}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+x+")"+e+"("+x+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(o){var r=$.attr(o,e);return null==r?"!="===t:!t||(r+="","="===t?r===n:"!="===t?r!==n:"^="===t?n&&0===r.indexOf(n):"*="===t?n&&r.indexOf(n)>-1:"$="===t?n&&r.slice(-n.length)===n:"~="===t?(" "+r.replace(P," ")+" ").indexOf(n)>-1:"|="===t&&(r===n||r.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,o,r){var a="nth"!==e.slice(0,3),i="last"!==e.slice(-4),c="of-type"===t;return 1===o&&0===r?function(e){return!!e.parentNode}:function(t,n,p){var l,s,u,b,M,d=a!==i?"nextSibling":"previousSibling",z=t.parentNode,O=c&&t.nodeName.toLowerCase(),A=!p&&!c,m=!1;if(z){if(a){for(;d;){for(u=t;u=u[d];)if(c?L(u,O):1===u.nodeType)return!1;M=d="only"===e&&!M&&"nextSibling"}return!0}if(M=[i?z.firstChild:z.lastChild],i&&A){for(m=(b=(l=(s=z[f]||(z[f]={}))[e]||[])[0]===h&&l[1])&&l[2],u=b&&z.childNodes[b];u=++b&&u&&u[d]||(m=b=0)||M.pop();)if(1===u.nodeType&&++m&&u===t){s[e]=[h,b,m];break}}else if(A&&(m=b=(l=(s=t[f]||(t[f]={}))[e]||[])[0]===h&&l[1]),!1===m)for(;(u=++b&&u&&u[d]||(m=b=0)||M.pop())&&(!(c?L(u,O):1===u.nodeType)||!++m||(A&&((s=u[f]||(u[f]={}))[e]=[h,m]),u!==t)););return(m-=r)===o||m%o===0&&m/o>=0}}},PSEUDO:function(e,n){var o,r=t.pseudos[e]||t.setFilters[e.toLowerCase()]||$.error("unsupported pseudo: "+e);return r[f]?r(n):r.length>1?(o=[e,e,"",n],t.setFilters.hasOwnProperty(e.toLowerCase())?te(function(e,t){for(var o,a=r(e,n),i=a.length;i--;)e[o=s.call(e,a[i])]=!(t[o]=a[i])}):function(e){return r(e,0,o)}):r}},pseudos:{not:te(function(e){var t=[],n=[],o=fe(e.replace(k,"$1"));return o[f]?te(function(e,t,n,r){for(var a,i=o(e,null,r,[]),c=e.length;c--;)(a=i[c])&&(e[c]=!(t[c]=a))}):function(e,r,a){return t[0]=e,o(t,null,a,n),t[0]=null,!n.pop()}}),has:te(function(e){return function(t){return $(e,t).length>0}}),contains:te(function(e){return e=e.replace(K,Z),function(t){return(t.textContent||g.text(t)).indexOf(e)>-1}}),lang:te(function(e){return F.test(e||"")||$.error("unsupported lang: "+e),e=e.replace(K,Z).toLowerCase(),function(t){var n;do{if(n=u?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(e){var t=o.location&&o.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===l},focus:function(e){return e===function(){try{return p.activeElement}catch(e){}}()&&p.hasFocus()&&!!(e.type||e.href||~e.tabIndex)},enabled:ae(!1),disabled:ae(!0),checked:function(e){return L(e,"input")&&!!e.checked||L(e,"option")&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!t.pseudos.empty(e)},header:function(e){return U.test(e.nodeName)},input:function(e){return I.test(e.nodeName)},button:function(e){return L(e,"input")&&"button"===e.type||L(e,"button")},text:function(e){var t;return L(e,"input")&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:ie(function(){return[0]}),last:ie(function(e,t){return[t-1]}),eq:ie(function(e,t,n){return[n<0?n+t:n]}),even:ie(function(e,t){for(var n=0;nt?t:n;--o>=0;)e.push(o);return e}),gt:ie(function(e,t,n){for(var o=n<0?n+t:n;++o1?function(t,n,o){for(var r=e.length;r--;)if(!e[r](t,n,o))return!1;return!0}:e[0]}function de(e,t,n,o,r){for(var a,i=[],c=0,p=e.length,l=null!=t;c-1&&(a[l]=!(i[l]=b))}}else M=de(M===i?M.splice(f,M.length):M),r?r(null,i,M,p):z.apply(i,M)})}function Oe(e){for(var o,r,a,i=e.length,c=t.relative[e[0].type],p=c||t.relative[" "],l=c?1:0,u=be(function(e){return e===o},p,!0),b=be(function(e){return s.call(o,e)>-1},p,!0),M=[function(e,t,r){var a=!c&&(r||t!=n)||((o=t).nodeType?u(e,t,r):b(e,t,r));return o=null,a}];l1&&Me(M),l>1&&ue(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(k,"$1"),r,l0,a=e.length>0,i=function(i,c,l,s,b){var M,d,O,f=0,A="0",m=i&&[],q=[],v=n,_=i||a&&t.find.TAG("*",b),y=h+=null==v?1:Math.random()||.1,W=_.length;for(b&&(n=c==p||c||b);A!==W&&null!=(M=_[A]);A++){if(a&&M){for(d=0,c||M.ownerDocument==p||(pe(M),l=!u);O=e[d++];)if(O(M,c||p,l)){z.call(s,M);break}b&&(h=y)}r&&((M=!O&&M)&&f--,i&&m.push(M))}if(f+=A,r&&A!==f){for(d=0;O=o[d++];)O(m,q,c,l);if(i){if(f>0)for(;A--;)m[A]||q[A]||(q[A]=R.call(s));q=de(q)}z.apply(s,q),b&&!i&&q.length>0&&f+o.length>1&&g.uniqueSort(s)}return b&&(h=y,n=v),m};return r?te(i):i}(i,a)),c.selector=e}return c}function he(e,n,o,r){var a,i,c,p,l,s="function"==typeof e&&e,b=!r&&se(e=s.selector||e);if(o=o||[],1===b.length){if((i=b[0]=b[0].slice(0)).length>2&&"ID"===(c=i[0]).type&&9===n.nodeType&&u&&t.relative[i[1].type]){if(!(n=(t.find.ID(c.matches[0].replace(K,Z),n)||[])[0]))return o;s&&(n=n.parentNode),e=e.slice(i.shift().value.length)}for(a=H.needsContext.test(e)?0:i.length;a--&&(c=i[a],!t.relative[p=c.type]);)if((l=t.find[p])&&(r=l(c.matches[0].replace(K,Z),V.test(i[0].type)&&ce(n.parentNode)||n))){if(i.splice(a,1),!(e=r.length&&ue(i)))return z.apply(o,r),o;break}}return(s||fe(e,b))(r,n,!u,o,!n||V.test(e)&&ce(n.parentNode)||n),o}le.prototype=t.filters=t.pseudos,t.setFilters=new le,O.sortStable=f.split("").sort(y).join("")===f,pe(),O.sortDetached=ne(function(e){return 1&e.compareDocumentPosition(p.createElement("fieldset"))}),g.find=$,g.expr[":"]=g.expr.pseudos,g.unique=g.uniqueSort,$.compile=fe,$.select=he,$.setDocument=pe,$.tokenize=se,$.escape=g.escapeSelector,$.getText=g.text,$.isXML=g.isXMLDoc,$.selectors=g.expr,$.support=g.support,$.uniqueSort=g.uniqueSort}();var D=function(e,t,n){for(var o=[],r=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(r&&g(e).is(n))break;o.push(e)}return o},P=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},C=g.expr.match.needsContext,j=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function Y(e,t,n){return f(t)?g.grep(e,function(e,o){return!!t.call(e,o,e)!==n}):t.nodeType?g.grep(e,function(e){return e===t!==n}):"string"!=typeof t?g.grep(e,function(e){return s.call(t,e)>-1!==n}):g.filter(t,e,n)}g.filter=function(e,t,n){var o=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===o.nodeType?g.find.matchesSelector(o,e)?[o]:[]:g.find.matches(e,g.grep(t,function(e){return 1===e.nodeType}))},g.fn.extend({find:function(e){var t,n,o=this.length,r=this;if("string"!=typeof e)return this.pushStack(g(e).filter(function(){for(t=0;t1?g.uniqueSort(n):n},filter:function(e){return this.pushStack(Y(this,e||[],!1))},not:function(e){return this.pushStack(Y(this,e||[],!0))},is:function(e){return!!Y(this,"string"==typeof e&&C.test(e)?g(e):e||[],!1).length}});var X,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(g.fn.init=function(e,t,n){var o,r;if(!e)return this;if(n=n||X,"string"==typeof e){if(!(o="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:F.exec(e))||!o[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(o[1]){if(t=t instanceof g?t[0]:t,g.merge(this,g.parseHTML(o[1],t&&t.nodeType?t.ownerDocument||t:A,!0)),j.test(o[1])&&g.isPlainObject(t))for(o in t)f(this[o])?this[o](t[o]):this.attr(o,t[o]);return this}return(r=A.getElementById(o[2]))&&(this[0]=r,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):f(e)?void 0!==n.ready?n.ready(e):e(g):g.makeArray(e,this)}).prototype=g.fn,X=g(A);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};function U(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}g.fn.extend({has:function(e){var t=g(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&g.find.matchesSelector(n,e))){a.push(n);break}return this.pushStack(a.length>1?g.uniqueSort(a):a)},index:function(e){return e?"string"==typeof e?s.call(g(e),this[0]):s.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(g.uniqueSort(g.merge(this.get(),g(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),g.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return D(e,"parentNode")},parentsUntil:function(e,t,n){return D(e,"parentNode",n)},next:function(e){return U(e,"nextSibling")},prev:function(e){return U(e,"previousSibling")},nextAll:function(e){return D(e,"nextSibling")},prevAll:function(e){return D(e,"previousSibling")},nextUntil:function(e,t,n){return D(e,"nextSibling",n)},prevUntil:function(e,t,n){return D(e,"previousSibling",n)},siblings:function(e){return P((e.parentNode||{}).firstChild,e)},children:function(e){return P(e.firstChild)},contents:function(e){return null!=e.contentDocument&&i(e.contentDocument)?e.contentDocument:(L(e,"template")&&(e=e.content||e),g.merge([],e.childNodes))}},function(e,t){g.fn[e]=function(n,o){var r=g.map(this,t,n);return"Until"!==e.slice(-5)&&(o=n),o&&"string"==typeof o&&(r=g.filter(o,r)),this.length>1&&(I[e]||g.uniqueSort(r),H.test(e)&&r.reverse()),this.pushStack(r)}});var G=/[^\x20\t\r\n\f]+/g;function V(e){return e}function K(e){throw e}function Z(e,t,n,o){var r;try{e&&f(r=e.promise)?r.call(e).done(t).fail(n):e&&f(r=e.then)?r.call(e,t,n):t.apply(void 0,[e].slice(o))}catch(e){n.apply(void 0,[e])}}g.Callbacks=function(e){e="string"==typeof e?function(e){var t={};return g.each(e.match(G)||[],function(e,n){t[n]=!0}),t}(e):g.extend({},e);var t,n,o,r,a=[],i=[],c=-1,p=function(){for(r=r||e.once,o=t=!0;i.length;c=-1)for(n=i.shift();++c-1;)a.splice(n,1),n<=c&&c--}),this},has:function(e){return e?g.inArray(e,a)>-1:a.length>0},empty:function(){return a&&(a=[]),this},disable:function(){return r=i=[],a=n="",this},disabled:function(){return!a},lock:function(){return r=i=[],n||t||(a=n=""),this},locked:function(){return!!r},fireWith:function(e,n){return r||(n=[e,(n=n||[]).slice?n.slice():n],i.push(n),t||p()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!o}};return l},g.extend({Deferred:function(e){var t=[["notify","progress",g.Callbacks("memory"),g.Callbacks("memory"),2],["resolve","done",g.Callbacks("once memory"),g.Callbacks("once memory"),0,"resolved"],["reject","fail",g.Callbacks("once memory"),g.Callbacks("once memory"),1,"rejected"]],n="pending",r={state:function(){return n},always:function(){return a.done(arguments).fail(arguments),this},catch:function(e){return r.then(null,e)},pipe:function(){var e=arguments;return g.Deferred(function(n){g.each(t,function(t,o){var r=f(e[o[4]])&&e[o[4]];a[o[1]](function(){var e=r&&r.apply(this,arguments);e&&f(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[o[0]+"With"](this,r?[e]:arguments)})}),e=null}).promise()},then:function(e,n,r){var a=0;function i(e,t,n,r){return function(){var c=this,p=arguments,l=function(){var o,l;if(!(e=a&&(n!==K&&(c=void 0,p=[o]),t.rejectWith(c,p))}};e?s():(g.Deferred.getErrorHook?s.error=g.Deferred.getErrorHook():g.Deferred.getStackHook&&(s.error=g.Deferred.getStackHook()),o.setTimeout(s))}}return g.Deferred(function(o){t[0][3].add(i(0,o,f(r)?r:V,o.notifyWith)),t[1][3].add(i(0,o,f(e)?e:V)),t[2][3].add(i(0,o,f(n)?n:K))}).promise()},promise:function(e){return null!=e?g.extend(e,r):r}},a={};return g.each(t,function(e,o){var i=o[2],c=o[5];r[o[1]]=i.add,c&&i.add(function(){n=c},t[3-e][2].disable,t[3-e][3].disable,t[0][2].lock,t[0][3].lock),i.add(o[3].fire),a[o[0]]=function(){return a[o[0]+"With"](this===a?void 0:this,arguments),this},a[o[0]+"With"]=i.fireWith}),r.promise(a),e&&e.call(a,a),a},when:function(e){var t=arguments.length,n=t,o=Array(n),r=c.call(arguments),a=g.Deferred(),i=function(e){return function(n){o[e]=this,r[e]=arguments.length>1?c.call(arguments):n,--t||a.resolveWith(o,r)}};if(t<=1&&(Z(e,a.done(i(n)).resolve,a.reject,!t),"pending"===a.state()||f(r[n]&&r[n].then)))return a.then();for(;n--;)Z(r[n],i(n),a.reject);return a.promise()}});var J=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;g.Deferred.exceptionHook=function(e,t){o.console&&o.console.warn&&e&&J.test(e.name)&&o.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},g.readyException=function(e){o.setTimeout(function(){throw e})};var Q=g.Deferred();function $(){A.removeEventListener("DOMContentLoaded",$),o.removeEventListener("load",$),g.ready()}g.fn.ready=function(e){return Q.then(e).catch(function(e){g.readyException(e)}),this},g.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--g.readyWait:g.isReady)||(g.isReady=!0,!0!==e&&--g.readyWait>0||Q.resolveWith(A,[g]))}}),g.ready.then=Q.then,"complete"===A.readyState||"loading"!==A.readyState&&!A.documentElement.doScroll?o.setTimeout(g.ready):(A.addEventListener("DOMContentLoaded",$),o.addEventListener("load",$));var ee=function(e,t,n,o,r,a,i){var c=0,p=e.length,l=null==n;if("object"===v(n))for(c in r=!0,n)ee(e,t,c,n[c],!0,a,i);else if(void 0!==o&&(r=!0,f(o)||(i=!0),l&&(i?(t.call(e,o),t=null):(l=t,t=function(e,t,n){return l.call(g(e),n)})),t))for(;c1,null,!0)},removeData:function(e){return this.each(function(){pe.remove(this,e)})}}),g.extend({queue:function(e,t,n){var o;if(e)return t=(t||"fx")+"queue",o=ce.get(e,t),n&&(!o||Array.isArray(n)?o=ce.access(e,t,g.makeArray(n)):o.push(n)),o||[]},dequeue:function(e,t){t=t||"fx";var n=g.queue(e,t),o=n.length,r=n.shift(),a=g._queueHooks(e,t);"inprogress"===r&&(r=n.shift(),o--),r&&("fx"===t&&n.unshift("inprogress"),delete a.stop,r.call(e,function(){g.dequeue(e,t)},a)),!o&&a&&a.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return ce.get(e,n)||ce.access(e,n,{empty:g.Callbacks("once memory").add(function(){ce.remove(e,[t+"queue",n])})})}}),g.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]*)/i,Le=/^$|^module$|\/(?:java|ecma)script/i;_e=A.createDocumentFragment().appendChild(A.createElement("div")),(ye=A.createElement("input")).setAttribute("type","radio"),ye.setAttribute("checked","checked"),ye.setAttribute("name","t"),_e.appendChild(ye),O.checkClone=_e.cloneNode(!0).cloneNode(!0).lastChild.checked,_e.innerHTML="",O.noCloneChecked=!!_e.cloneNode(!0).lastChild.defaultValue,_e.innerHTML="",O.option=!!_e.lastChild;var Re={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function we(e,t){var n;return n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&L(e,t)?g.merge([e],n):n}function Se(e,t){for(var n=0,o=e.length;n",""]);var xe=/<|&#?\w+;/;function ke(e,t,n,o,r){for(var a,i,c,p,l,s,u=t.createDocumentFragment(),b=[],M=0,d=e.length;M-1)r&&r.push(a);else if(l=Oe(a),i=we(u.appendChild(a),"script"),l&&Se(i),n)for(s=0;a=i[s++];)Le.test(a.type||"")&&n.push(a);return u}var Te=/^([^.]*)(?:\.(.+)|)/;function Ne(){return!0}function Ee(){return!1}function Be(e,t,n,o,r,a){var i,c;if("object"==typeof t){for(c in"string"!=typeof n&&(o=o||n,n=void 0),t)Be(e,c,n,o,t[c],a);return e}if(null==o&&null==r?(r=n,o=n=void 0):null==r&&("string"==typeof n?(r=o,o=void 0):(r=o,o=n,n=void 0)),!1===r)r=Ee;else if(!r)return e;return 1===a&&(i=r,r=function(e){return g().off(e),i.apply(this,arguments)},r.guid=i.guid||(i.guid=g.guid++)),e.each(function(){g.event.add(this,t,r,o,n)})}function De(e,t,n){n?(ce.set(e,t,!1),g.event.add(e,t,{namespace:!1,handler:function(e){var n,o=ce.get(this,t);if(1&e.isTrigger&&this[t]){if(o)(g.event.special[t]||{}).delegateType&&e.stopPropagation();else if(o=c.call(arguments),ce.set(this,t,o),this[t](),n=ce.get(this,t),ce.set(this,t,!1),o!==n)return e.stopImmediatePropagation(),e.preventDefault(),n}else o&&(ce.set(this,t,g.event.trigger(o[0],o.slice(1),this)),e.stopPropagation(),e.isImmediatePropagationStopped=Ne)}})):void 0===ce.get(e,t)&&g.event.add(e,t,Ne)}g.event={global:{},add:function(e,t,n,o,r){var a,i,c,p,l,s,u,b,M,d,z,O=ce.get(e);if(ae(e))for(n.handler&&(n=(a=n).handler,r=a.selector),r&&g.find.matchesSelector(ze,r),n.guid||(n.guid=g.guid++),(p=O.events)||(p=O.events=Object.create(null)),(i=O.handle)||(i=O.handle=function(t){return void 0!==g&&g.event.triggered!==t.type?g.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(G)||[""]).length;l--;)M=z=(c=Te.exec(t[l])||[])[1],d=(c[2]||"").split(".").sort(),M&&(u=g.event.special[M]||{},M=(r?u.delegateType:u.bindType)||M,u=g.event.special[M]||{},s=g.extend({type:M,origType:z,data:o,handler:n,guid:n.guid,selector:r,needsContext:r&&g.expr.match.needsContext.test(r),namespace:d.join(".")},a),(b=p[M])||((b=p[M]=[]).delegateCount=0,u.setup&&!1!==u.setup.call(e,o,d,i)||e.addEventListener&&e.addEventListener(M,i)),u.add&&(u.add.call(e,s),s.handler.guid||(s.handler.guid=n.guid)),r?b.splice(b.delegateCount++,0,s):b.push(s),g.event.global[M]=!0)},remove:function(e,t,n,o,r){var a,i,c,p,l,s,u,b,M,d,z,O=ce.hasData(e)&&ce.get(e);if(O&&(p=O.events)){for(l=(t=(t||"").match(G)||[""]).length;l--;)if(M=z=(c=Te.exec(t[l])||[])[1],d=(c[2]||"").split(".").sort(),M){for(u=g.event.special[M]||{},b=p[M=(o?u.delegateType:u.bindType)||M]||[],c=c[2]&&new RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=a=b.length;a--;)s=b[a],!r&&z!==s.origType||n&&n.guid!==s.guid||c&&!c.test(s.namespace)||o&&o!==s.selector&&("**"!==o||!s.selector)||(b.splice(a,1),s.selector&&b.delegateCount--,u.remove&&u.remove.call(e,s));i&&!b.length&&(u.teardown&&!1!==u.teardown.call(e,d,O.handle)||g.removeEvent(e,M,O.handle),delete p[M])}else for(M in p)g.event.remove(e,M+t[l],n,o,!0);g.isEmptyObject(p)&&ce.remove(e,"handle events")}},dispatch:function(e){var t,n,o,r,a,i,c=new Array(arguments.length),p=g.event.fix(e),l=(ce.get(this,"events")||Object.create(null))[p.type]||[],s=g.event.special[p.type]||{};for(c[0]=p,t=1;t=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(a=[],i={},n=0;n-1:g.find(r,this,null,[l]).length),i[r]&&a.push(o);a.length&&c.push({elem:l,handlers:a})}return l=this,p\s*$/g;function Ye(e,t){return L(e,"table")&&L(11!==t.nodeType?t:t.firstChild,"tr")&&g(e).children("tbody")[0]||e}function Xe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Fe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function He(e,t){var n,o,r,a,i,c;if(1===t.nodeType){if(ce.hasData(e)&&(c=ce.get(e).events))for(r in ce.remove(t,"handle events"),c)for(n=0,o=c[r].length;n1&&"string"==typeof d&&!O.checkClone&&Ce.test(d))return e.each(function(r){var a=e.eq(r);z&&(t[0]=d.call(this,r,a.html())),Ue(a,t,n,o)});if(b&&(a=(r=ke(t,e[0].ownerDocument,!1,e,o)).firstChild,1===r.childNodes.length&&(r=a),a||o)){for(c=(i=g.map(we(r,"script"),Xe)).length;u0&&Se(i,!p&&we(e,"script")),c},cleanData:function(e){for(var t,n,o,r=g.event.special,a=0;void 0!==(n=e[a]);a++)if(ae(n)){if(t=n[ce.expando]){if(t.events)for(o in t.events)r[o]?g.event.remove(n,o):g.removeEvent(n,o,t.handle);n[ce.expando]=void 0}n[pe.expando]&&(n[pe.expando]=void 0)}}}),g.fn.extend({detach:function(e){return Ge(this,e,!0)},remove:function(e){return Ge(this,e)},text:function(e){return ee(this,function(e){return void 0===e?g.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Ue(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Ye(this,e).appendChild(e)})},prepend:function(){return Ue(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Ye(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Ue(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Ue(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(g.cleanData(we(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return g.clone(this,e,t)})},html:function(e){return ee(this,function(e){var t=this[0]||{},n=0,o=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Pe.test(e)&&!Re[(We.exec(e)||["",""])[1].toLowerCase()]){e=g.htmlPrefilter(e);try{for(;n=0&&(p+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-a-p-c-.5))||0),p+l}function st(e,t,n){var o=Ze(e),r=(!O.boxSizingReliable()||n)&&"border-box"===g.css(e,"boxSizing",!1,o),a=r,i=$e(e,t,o),c="offset"+t[0].toUpperCase()+t.slice(1);if(Ve.test(i)){if(!n)return i;i="auto"}return(!O.boxSizingReliable()&&r||!O.reliableTrDimensions()&&L(e,"tr")||"auto"===i||!parseFloat(i)&&"inline"===g.css(e,"display",!1,o))&&e.getClientRects().length&&(r="border-box"===g.css(e,"boxSizing",!1,o),(a=c in e)&&(i=e[c])),(i=parseFloat(i)||0)+lt(e,t,n||(r?"border":"content"),a,o,i)+"px"}function ut(e,t,n,o,r){return new ut.prototype.init(e,t,n,o,r)}g.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=$e(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,aspectRatio:!0,borderImageSlice:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,scale:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeMiterlimit:!0,strokeOpacity:!0},cssProps:{},style:function(e,t,n,o){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var r,a,i,c=re(t),p=Ke.test(t),l=e.style;if(p||(t=rt(c)),i=g.cssHooks[t]||g.cssHooks[c],void 0===n)return i&&"get"in i&&void 0!==(r=i.get(e,!1,o))?r:l[t];"string"==(a=typeof n)&&(r=Me.exec(n))&&r[1]&&(n=Ae(e,t,r),a="number"),null!=n&&n==n&&("number"!==a||p||(n+=r&&r[3]||(g.cssNumber[c]?"":"px")),O.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),i&&"set"in i&&void 0===(n=i.set(e,n,o))||(p?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,o){var r,a,i,c=re(t);return Ke.test(t)||(t=rt(c)),(i=g.cssHooks[t]||g.cssHooks[c])&&"get"in i&&(r=i.get(e,!0,n)),void 0===r&&(r=$e(e,t,o)),"normal"===r&&t in ct&&(r=ct[t]),""===n||n?(a=parseFloat(r),!0===n||isFinite(a)?a||0:r):r}}),g.each(["height","width"],function(e,t){g.cssHooks[t]={get:function(e,n,o){if(n)return!at.test(g.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?st(e,t,o):Je(e,it,function(){return st(e,t,o)})},set:function(e,n,o){var r,a=Ze(e),i=!O.scrollboxSize()&&"absolute"===a.position,c=(i||o)&&"border-box"===g.css(e,"boxSizing",!1,a),p=o?lt(e,t,o,c,a):0;return c&&i&&(p-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(a[t])-lt(e,t,"border",!1,a)-.5)),p&&(r=Me.exec(n))&&"px"!==(r[3]||"px")&&(e.style[t]=n,n=g.css(e,t)),pt(0,n,p)}}}),g.cssHooks.marginLeft=et(O.reliableMarginLeft,function(e,t){if(t)return(parseFloat($e(e,"marginLeft"))||e.getBoundingClientRect().left-Je(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),g.each({margin:"",padding:"",border:"Width"},function(e,t){g.cssHooks[e+t]={expand:function(n){for(var o=0,r={},a="string"==typeof n?n.split(" "):[n];o<4;o++)r[e+de[o]+t]=a[o]||a[o-2]||a[0];return r}},"margin"!==e&&(g.cssHooks[e+t].set=pt)}),g.fn.extend({css:function(e,t){return ee(this,function(e,t,n){var o,r,a={},i=0;if(Array.isArray(t)){for(o=Ze(e),r=t.length;i1)}}),g.Tween=ut,ut.prototype={constructor:ut,init:function(e,t,n,o,r,a){this.elem=e,this.prop=n,this.easing=r||g.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=o,this.unit=a||(g.cssNumber[n]?"":"px")},cur:function(){var e=ut.propHooks[this.prop];return e&&e.get?e.get(this):ut.propHooks._default.get(this)},run:function(e){var t,n=ut.propHooks[this.prop];return this.options.duration?this.pos=t=g.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):ut.propHooks._default.set(this),this}},ut.prototype.init.prototype=ut.prototype,ut.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=g.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){g.fx.step[e.prop]?g.fx.step[e.prop](e):1!==e.elem.nodeType||!g.cssHooks[e.prop]&&null==e.elem.style[rt(e.prop)]?e.elem[e.prop]=e.now:g.style(e.elem,e.prop,e.now+e.unit)}}},ut.propHooks.scrollTop=ut.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},g.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},g.fx=ut.prototype.init,g.fx.step={};var bt,Mt,dt=/^(?:toggle|show|hide)$/,zt=/queueHooks$/;function Ot(){Mt&&(!1===A.hidden&&o.requestAnimationFrame?o.requestAnimationFrame(Ot):o.setTimeout(Ot,g.fx.interval),g.fx.tick())}function ft(){return o.setTimeout(function(){bt=void 0}),bt=Date.now()}function ht(e,t){var n,o=0,r={height:e};for(t=t?1:0;o<4;o+=2-t)r["margin"+(n=de[o])]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function At(e,t,n){for(var o,r=(mt.tweeners[t]||[]).concat(mt.tweeners["*"]),a=0,i=r.length;a1)},removeAttr:function(e){return this.each(function(){g.removeAttr(this,e)})}}),g.extend({attr:function(e,t,n){var o,r,a=e.nodeType;if(3!==a&&8!==a&&2!==a)return void 0===e.getAttribute?g.prop(e,t,n):(1===a&&g.isXMLDoc(e)||(r=g.attrHooks[t.toLowerCase()]||(g.expr.match.bool.test(t)?qt:void 0)),void 0!==n?null===n?void g.removeAttr(e,t):r&&"set"in r&&void 0!==(o=r.set(e,n,t))?o:(e.setAttribute(t,n+""),n):r&&"get"in r&&null!==(o=r.get(e,t))?o:null==(o=g.find.attr(e,t))?void 0:o)},attrHooks:{type:{set:function(e,t){if(!O.radioValue&&"radio"===t&&L(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,o=0,r=t&&t.match(G);if(r&&1===e.nodeType)for(;n=r[o++];)e.removeAttribute(n)}}),qt={set:function(e,t,n){return!1===t?g.removeAttr(e,n):e.setAttribute(n,n),n}},g.each(g.expr.match.bool.source.match(/\w+/g),function(e,t){var n=vt[t]||g.find.attr;vt[t]=function(e,t,o){var r,a,i=t.toLowerCase();return o||(a=vt[i],vt[i]=r,r=null!=n(e,t,o)?i:null,vt[i]=a),r}});var _t=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;function gt(e){return(e.match(G)||[]).join(" ")}function Wt(e){return e.getAttribute&&e.getAttribute("class")||""}function Lt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(G)||[]}g.fn.extend({prop:function(e,t){return ee(this,g.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[g.propFix[e]||e]})}}),g.extend({prop:function(e,t,n){var o,r,a=e.nodeType;if(3!==a&&8!==a&&2!==a)return 1===a&&g.isXMLDoc(e)||(t=g.propFix[t]||t,r=g.propHooks[t]),void 0!==n?r&&"set"in r&&void 0!==(o=r.set(e,n,t))?o:e[t]=n:r&&"get"in r&&null!==(o=r.get(e,t))?o:e[t]},propHooks:{tabIndex:{get:function(e){var t=g.find.attr(e,"tabindex");return t?parseInt(t,10):_t.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),O.optSelected||(g.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),g.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){g.propFix[this.toLowerCase()]=this}),g.fn.extend({addClass:function(e){var t,n,o,r,a,i;return f(e)?this.each(function(t){g(this).addClass(e.call(this,t,Wt(this)))}):(t=Lt(e)).length?this.each(function(){if(o=Wt(this),n=1===this.nodeType&&" "+gt(o)+" "){for(a=0;a-1;)n=n.replace(" "+r+" "," ");i=gt(n),o!==i&&this.setAttribute("class",i)}}):this:this.attr("class","")},toggleClass:function(e,t){var n,o,r,a,i=typeof e,c="string"===i||Array.isArray(e);return f(e)?this.each(function(n){g(this).toggleClass(e.call(this,n,Wt(this),t),t)}):"boolean"==typeof t&&c?t?this.addClass(e):this.removeClass(e):(n=Lt(e),this.each(function(){if(c)for(a=g(this),r=0;r-1)return!0;return!1}});var Rt=/\r/g;g.fn.extend({val:function(e){var t,n,o,r=this[0];return arguments.length?(o=f(e),this.each(function(n){var r;1===this.nodeType&&(null==(r=o?e.call(this,n,g(this).val()):e)?r="":"number"==typeof r?r+="":Array.isArray(r)&&(r=g.map(r,function(e){return null==e?"":e+""})),(t=g.valHooks[this.type]||g.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,r,"value")||(this.value=r))})):r?(t=g.valHooks[r.type]||g.valHooks[r.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(r,"value"))?n:"string"==typeof(n=r.value)?n.replace(Rt,""):null==n?"":n:void 0}}),g.extend({valHooks:{option:{get:function(e){var t=g.find.attr(e,"value");return null!=t?t:gt(g.text(e))}},select:{get:function(e){var t,n,o,r=e.options,a=e.selectedIndex,i="select-one"===e.type,c=i?null:[],p=i?a+1:r.length;for(o=a<0?p:i?a:0;o-1)&&(n=!0);return n||(e.selectedIndex=-1),a}}}}),g.each(["radio","checkbox"],function(){g.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=g.inArray(g(e).val(),t)>-1}},O.checkOn||(g.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var wt=o.location,St={guid:Date.now()},xt=/\?/;g.parseXML=function(e){var t,n;if(!e||"string"!=typeof e)return null;try{t=(new o.DOMParser).parseFromString(e,"text/xml")}catch(e){}return n=t&&t.getElementsByTagName("parsererror")[0],t&&!n||g.error("Invalid XML: "+(n?g.map(n.childNodes,function(e){return e.textContent}).join("\n"):e)),t};var kt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};g.extend(g.event,{trigger:function(e,t,n,r){var a,i,c,p,l,s,u,b,d=[n||A],z=M.call(e,"type")?e.type:e,O=M.call(e,"namespace")?e.namespace.split("."):[];if(i=b=c=n=n||A,3!==n.nodeType&&8!==n.nodeType&&!kt.test(z+g.event.triggered)&&(z.indexOf(".")>-1&&(O=z.split("."),z=O.shift(),O.sort()),l=z.indexOf(":")<0&&"on"+z,(e=e[g.expando]?e:new g.Event(z,"object"==typeof e&&e)).isTrigger=r?2:3,e.namespace=O.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+O.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=n),t=null==t?[e]:g.makeArray(t,[e]),u=g.event.special[z]||{},r||!u.trigger||!1!==u.trigger.apply(n,t))){if(!r&&!u.noBubble&&!h(n)){for(p=u.delegateType||z,kt.test(p+z)||(i=i.parentNode);i;i=i.parentNode)d.push(i),c=i;c===(n.ownerDocument||A)&&d.push(c.defaultView||c.parentWindow||o)}for(a=0;(i=d[a++])&&!e.isPropagationStopped();)b=i,e.type=a>1?p:u.bindType||z,(s=(ce.get(i,"events")||Object.create(null))[e.type]&&ce.get(i,"handle"))&&s.apply(i,t),(s=l&&i[l])&&s.apply&&ae(i)&&(e.result=s.apply(i,t),!1===e.result&&e.preventDefault());return e.type=z,r||e.isDefaultPrevented()||u._default&&!1!==u._default.apply(d.pop(),t)||!ae(n)||l&&f(n[z])&&!h(n)&&((c=n[l])&&(n[l]=null),g.event.triggered=z,e.isPropagationStopped()&&b.addEventListener(z,Tt),n[z](),e.isPropagationStopped()&&b.removeEventListener(z,Tt),g.event.triggered=void 0,c&&(n[l]=c)),e.result}},simulate:function(e,t,n){var o=g.extend(new g.Event,n,{type:e,isSimulated:!0});g.event.trigger(o,null,t)}}),g.fn.extend({trigger:function(e,t){return this.each(function(){g.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return g.event.trigger(e,t,n,!0)}});var Nt=/\[\]$/,Et=/\r?\n/g,Bt=/^(?:submit|button|image|reset|file)$/i,Dt=/^(?:input|select|textarea|keygen)/i;function Pt(e,t,n,o){var r;if(Array.isArray(t))g.each(t,function(t,r){n||Nt.test(e)?o(e,r):Pt(e+"["+("object"==typeof r&&null!=r?t:"")+"]",r,n,o)});else if(n||"object"!==v(t))o(e,t);else for(r in t)Pt(e+"["+r+"]",t[r],n,o)}g.param=function(e,t){var n,o=[],r=function(e,t){var n=f(t)?t():t;o[o.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!g.isPlainObject(e))g.each(e,function(){r(this.name,this.value)});else for(n in e)Pt(n,e[n],t,r);return o.join("&")},g.fn.extend({serialize:function(){return g.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=g.prop(this,"elements");return e?g.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!g(this).is(":disabled")&&Dt.test(this.nodeName)&&!Bt.test(e)&&(this.checked||!ge.test(e))}).map(function(e,t){var n=g(this).val();return null==n?null:Array.isArray(n)?g.map(n,function(e){return{name:t.name,value:e.replace(Et,"\r\n")}}):{name:t.name,value:n.replace(Et,"\r\n")}}).get()}});var Ct=/%20/g,jt=/#.*$/,Yt=/([?&])_=[^&]*/,Xt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Ft=/^(?:GET|HEAD)$/,Ht=/^\/\//,It={},Ut={},Gt="*/".concat("*"),Vt=A.createElement("a");function Kt(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var o,r=0,a=t.toLowerCase().match(G)||[];if(f(n))for(;o=a[r++];)"+"===o[0]?(o=o.slice(1)||"*",(e[o]=e[o]||[]).unshift(n)):(e[o]=e[o]||[]).push(n)}}function Zt(e,t,n,o){var r={},a=e===Ut;function i(c){var p;return r[c]=!0,g.each(e[c]||[],function(e,c){var l=c(t,n,o);return"string"!=typeof l||a||r[l]?a?!(p=l):void 0:(t.dataTypes.unshift(l),i(l),!1)}),p}return i(t.dataTypes[0])||!r["*"]&&i("*")}function Jt(e,t){var n,o,r=g.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((r[n]?e:o||(o={}))[n]=t[n]);return o&&g.extend(!0,e,o),e}Vt.href=wt.href,g.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:wt.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(wt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Gt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":g.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Jt(Jt(e,g.ajaxSettings),t):Jt(g.ajaxSettings,e)},ajaxPrefilter:Kt(It),ajaxTransport:Kt(Ut),ajax:function(e,t){"object"==typeof e&&(t=e,e=void 0),t=t||{};var n,r,a,i,c,p,l,s,u,b,M=g.ajaxSetup({},t),d=M.context||M,z=M.context&&(d.nodeType||d.jquery)?g(d):g.event,O=g.Deferred(),f=g.Callbacks("once memory"),h=M.statusCode||{},m={},q={},v="canceled",_={readyState:0,getResponseHeader:function(e){var t;if(l){if(!i)for(i={};t=Xt.exec(a);)i[t[1].toLowerCase()+" "]=(i[t[1].toLowerCase()+" "]||[]).concat(t[2]);t=i[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return l?a:null},setRequestHeader:function(e,t){return null==l&&(e=q[e.toLowerCase()]=q[e.toLowerCase()]||e,m[e]=t),this},overrideMimeType:function(e){return null==l&&(M.mimeType=e),this},statusCode:function(e){var t;if(e)if(l)_.always(e[_.status]);else for(t in e)h[t]=[h[t],e[t]];return this},abort:function(e){var t=e||v;return n&&n.abort(t),y(0,t),this}};if(O.promise(_),M.url=((e||M.url||wt.href)+"").replace(Ht,wt.protocol+"//"),M.type=t.method||t.type||M.method||M.type,M.dataTypes=(M.dataType||"*").toLowerCase().match(G)||[""],null==M.crossDomain){p=A.createElement("a");try{p.href=M.url,p.href=p.href,M.crossDomain=Vt.protocol+"//"+Vt.host!=p.protocol+"//"+p.host}catch(e){M.crossDomain=!0}}if(M.data&&M.processData&&"string"!=typeof M.data&&(M.data=g.param(M.data,M.traditional)),Zt(It,M,t,_),l)return _;for(u in(s=g.event&&M.global)&&0===g.active++&&g.event.trigger("ajaxStart"),M.type=M.type.toUpperCase(),M.hasContent=!Ft.test(M.type),r=M.url.replace(jt,""),M.hasContent?M.data&&M.processData&&0===(M.contentType||"").indexOf("application/x-www-form-urlencoded")&&(M.data=M.data.replace(Ct,"+")):(b=M.url.slice(r.length),M.data&&(M.processData||"string"==typeof M.data)&&(r+=(xt.test(r)?"&":"?")+M.data,delete M.data),!1===M.cache&&(r=r.replace(Yt,"$1"),b=(xt.test(r)?"&":"?")+"_="+St.guid+++b),M.url=r+b),M.ifModified&&(g.lastModified[r]&&_.setRequestHeader("If-Modified-Since",g.lastModified[r]),g.etag[r]&&_.setRequestHeader("If-None-Match",g.etag[r])),(M.data&&M.hasContent&&!1!==M.contentType||t.contentType)&&_.setRequestHeader("Content-Type",M.contentType),_.setRequestHeader("Accept",M.dataTypes[0]&&M.accepts[M.dataTypes[0]]?M.accepts[M.dataTypes[0]]+("*"!==M.dataTypes[0]?", "+Gt+"; q=0.01":""):M.accepts["*"]),M.headers)_.setRequestHeader(u,M.headers[u]);if(M.beforeSend&&(!1===M.beforeSend.call(d,_,M)||l))return _.abort();if(v="abort",f.add(M.complete),_.done(M.success),_.fail(M.error),n=Zt(Ut,M,t,_)){if(_.readyState=1,s&&z.trigger("ajaxSend",[_,M]),l)return _;M.async&&M.timeout>0&&(c=o.setTimeout(function(){_.abort("timeout")},M.timeout));try{l=!1,n.send(m,y)}catch(e){if(l)throw e;y(-1,e)}}else y(-1,"No Transport");function y(e,t,i,p){var u,b,A,m,q,v=t;l||(l=!0,c&&o.clearTimeout(c),n=void 0,a=p||"",_.readyState=e>0?4:0,u=e>=200&&e<300||304===e,i&&(m=function(e,t,n){for(var o,r,a,i,c=e.contents,p=e.dataTypes;"*"===p[0];)p.shift(),void 0===o&&(o=e.mimeType||t.getResponseHeader("Content-Type"));if(o)for(r in c)if(c[r]&&c[r].test(o)){p.unshift(r);break}if(p[0]in n)a=p[0];else{for(r in n){if(!p[0]||e.converters[r+" "+p[0]]){a=r;break}i||(i=r)}a=a||i}if(a)return a!==p[0]&&p.unshift(a),n[a]}(M,_,i)),!u&&g.inArray("script",M.dataTypes)>-1&&g.inArray("json",M.dataTypes)<0&&(M.converters["text script"]=function(){}),m=function(e,t,n,o){var r,a,i,c,p,l={},s=e.dataTypes.slice();if(s[1])for(i in e.converters)l[i.toLowerCase()]=e.converters[i];for(a=s.shift();a;)if(e.responseFields[a]&&(n[e.responseFields[a]]=t),!p&&o&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),p=a,a=s.shift())if("*"===a)a=p;else if("*"!==p&&p!==a){if(!(i=l[p+" "+a]||l["* "+a]))for(r in l)if((c=r.split(" "))[1]===a&&(i=l[p+" "+c[0]]||l["* "+c[0]])){!0===i?i=l[r]:!0!==l[r]&&(a=c[0],s.unshift(c[1]));break}if(!0!==i)if(i&&e.throws)t=i(t);else try{t=i(t)}catch(e){return{state:"parsererror",error:i?e:"No conversion from "+p+" to "+a}}}return{state:"success",data:t}}(M,m,_,u),u?(M.ifModified&&((q=_.getResponseHeader("Last-Modified"))&&(g.lastModified[r]=q),(q=_.getResponseHeader("etag"))&&(g.etag[r]=q)),204===e||"HEAD"===M.type?v="nocontent":304===e?v="notmodified":(v=m.state,b=m.data,u=!(A=m.error))):(A=v,!e&&v||(v="error",e<0&&(e=0))),_.status=e,_.statusText=(t||v)+"",u?O.resolveWith(d,[b,v,_]):O.rejectWith(d,[_,v,A]),_.statusCode(h),h=void 0,s&&z.trigger(u?"ajaxSuccess":"ajaxError",[_,M,u?b:A]),f.fireWith(d,[_,v]),s&&(z.trigger("ajaxComplete",[_,M]),--g.active||g.event.trigger("ajaxStop")))}return _},getJSON:function(e,t,n){return g.get(e,t,n,"json")},getScript:function(e,t){return g.get(e,void 0,t,"script")}}),g.each(["get","post"],function(e,t){g[t]=function(e,n,o,r){return f(n)&&(r=r||o,o=n,n=void 0),g.ajax(g.extend({url:e,type:t,dataType:r,data:n,success:o},g.isPlainObject(e)&&e))}}),g.ajaxPrefilter(function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")}),g._evalUrl=function(e,t,n){return g.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){g.globalEval(e,t,n)}})},g.fn.extend({wrapAll:function(e){var t;return this[0]&&(f(e)&&(e=e.call(this[0])),t=g(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return f(e)?this.each(function(t){g(this).wrapInner(e.call(this,t))}):this.each(function(){var t=g(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=f(e);return this.each(function(n){g(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){g(this).replaceWith(this.childNodes)}),this}}),g.expr.pseudos.hidden=function(e){return!g.expr.pseudos.visible(e)},g.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},g.ajaxSettings.xhr=function(){try{return new o.XMLHttpRequest}catch(e){}};var Qt={0:200,1223:204},$t=g.ajaxSettings.xhr();O.cors=!!$t&&"withCredentials"in $t,O.ajax=$t=!!$t,g.ajaxTransport(function(e){var t,n;if(O.cors||$t&&!e.crossDomain)return{send:function(r,a){var i,c=e.xhr();if(c.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)c[i]=e.xhrFields[i];for(i in e.mimeType&&c.overrideMimeType&&c.overrideMimeType(e.mimeType),e.crossDomain||r["X-Requested-With"]||(r["X-Requested-With"]="XMLHttpRequest"),r)c.setRequestHeader(i,r[i]);t=function(e){return function(){t&&(t=n=c.onload=c.onerror=c.onabort=c.ontimeout=c.onreadystatechange=null,"abort"===e?c.abort():"error"===e?"number"!=typeof c.status?a(0,"error"):a(c.status,c.statusText):a(Qt[c.status]||c.status,c.statusText,"text"!==(c.responseType||"text")||"string"!=typeof c.responseText?{binary:c.response}:{text:c.responseText},c.getAllResponseHeaders()))}},c.onload=t(),n=c.onerror=c.ontimeout=t("error"),void 0!==c.onabort?c.onabort=n:c.onreadystatechange=function(){4===c.readyState&&o.setTimeout(function(){t&&n()})},t=t("abort");try{c.send(e.hasContent&&e.data||null)}catch(e){if(t)throw e}},abort:function(){t&&t()}}}),g.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),g.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return g.globalEval(e),e}}}),g.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),g.ajaxTransport("script",function(e){var t,n;if(e.crossDomain||e.scriptAttrs)return{send:function(o,r){t=g("