diff --git a/Source/Libraries/GSF.PhasorProtocols/UI/DataModels/InputWizardDevice.cs b/Source/Libraries/GSF.PhasorProtocols/UI/DataModels/InputWizardDevice.cs index b317208d611..4e2b2bbd92d 100755 --- a/Source/Libraries/GSF.PhasorProtocols/UI/DataModels/InputWizardDevice.cs +++ b/Source/Libraries/GSF.PhasorProtocols/UI/DataModels/InputWizardDevice.cs @@ -42,10 +42,12 @@ public class InputWizardDevice : DataModelBase #region [ Members ] // Fields + private int m_id; private string m_acronym; private string m_name; private string m_configAcronym; private string m_configName; + private string m_linkAcronym; private decimal m_longitude; private decimal m_latitude; private int? m_vendorDeviceId; @@ -56,24 +58,43 @@ public class InputWizardDevice : DataModelBase private int m_analogCount; private bool m_addDigitals; private bool m_addAnalogs; - private bool m_existing; + private bool m_hasConflict; private string m_statusColor; private ObservableCollection m_phasorList; + private bool m_useConfigLabels; #endregion #region [ Properties ] /// - /// Gets or sets existing device ID, if any. + /// Gets or sets ID of the existing database record, if any. /// - public int ID { get; set; } + public int ID + { + get => m_id; + set + { + m_id = value; + UpdateStatusColor(); + } + } /// /// Gets or sets and provided unique ID for the device. /// public Guid? UniqueID { get; set; } + /// + /// Gets or sets and provided global ID for the device's configuration cell. + /// + public Guid? GlobalID3 { get; set; } + + /// + /// Gets or sets the acronym of the existing device record. + /// + public string OldAcronym { get; set; } + /// /// Gets or sets acronym of the . /// @@ -91,7 +112,15 @@ public string Acronym m_acronym = m_acronym.Substring(0, 200); OnPropertyChanged(nameof(Acronym)); - Existing = Device.GetDevice(null, $"WHERE Acronym = '{m_acronym.ToUpper()}'") is not null; + + HasConflict = + m_acronym != OldAcronym && + Device.GetDevice(null, $"WHERE Acronym = '{m_acronym.ToUpper()}'") is not null; + + if (UseConfigLabels) + ConfigFrameAcronym = value; + else + DatabaseAcronym = value; } } @@ -106,6 +135,11 @@ public string Name { m_name = value is null || value.Length <= 200 ? value : value.Substring(0, 200); OnPropertyChanged(nameof(Name)); + + if (UseConfigLabels) + ConfigFrameName = value; + else + DatabaseName = value; } } @@ -135,6 +169,19 @@ public string ConfigName } } + /// + /// Gets or sets tooltip info describing the device acronym from the database. + /// + public string LinkAcronym + { + get => m_linkAcronym; + set + { + m_linkAcronym = value; + OnPropertyChanged(nameof(LinkAcronym)); + } + } + /// /// Gets or sets Longitude. /// @@ -268,14 +315,14 @@ public bool AddAnalogs /// /// Gets or sets existing flag. /// - public bool Existing + public bool HasConflict { - get => m_existing; + get => m_hasConflict; set { - m_existing = value; - OnPropertyChanged(nameof(Existing)); - StatusColor = m_existing ? "green" : "transparent"; + m_hasConflict = value; + OnPropertyChanged(nameof(HasConflict)); + UpdateStatusColor(); } } @@ -289,9 +336,16 @@ public string StatusColor { m_statusColor = value; OnPropertyChanged(nameof(StatusColor)); + OnPropertyChanged(nameof(IsHighlighted)); } } + /// + /// Gets flag to indicate whether the device is highlighted via . + /// + public bool IsHighlighted => + StatusColor != "transparent"; + /// /// Gets or sets phasor list. /// @@ -380,10 +434,63 @@ public string DigitalLabelsPreview /// public string AnalogLabelsPreview => AnalogLabels is not null ? string.Join(Environment.NewLine, AnalogLabels.Select((label, index) => $"Analog {index}: {label}")) : string.Empty; + internal string DatabaseAcronym { get; set; } + internal string ConfigFrameAcronym { get; set; } + internal string DatabaseName { get; set; } + internal string ConfigFrameName { get; set; } + + internal bool UseConfigLabels + { + get => m_useConfigLabels; + set + { + if (m_useConfigLabels == value) return; + m_useConfigLabels = value; + Acronym = m_useConfigLabels ? ConfigFrameAcronym : DatabaseAcronym; + Name = m_useConfigLabels ? ConfigFrameName : DatabaseName; + + foreach (InputWizardDevicePhasor phasor in PhasorList) + phasor.UseConfigLabels = value; + } + } + #endregion #region [ Methods ] + /// + /// Detaches the input wizard device from the database record it was mapped to. + /// + public void Unlink() + { + ID = 0; + UniqueID = GlobalID3; + OldAcronym = null; + VendorDeviceID = null; + DatabaseAcronym = ConfigFrameAcronym; + DatabaseName = ConfigFrameName; + + if (!UseConfigLabels) + { + Acronym = ConfigFrameAcronym; + Name = ConfigFrameName; + } + + foreach (InputWizardDevicePhasor phasor in m_phasorList) + { + phasor.DatabaseLabel = phasor.ConfigFrameLabel; + phasor.DatabaseType = phasor.ConfigFrameType; + phasor.DatabasePhase = phasor.ConfigFramePhase; + + if (!phasor.UseConfigLabels) + { + phasor.Label = phasor.ConfigFrameLabel; + phasor.Type = phasor.ConfigFrameType; + phasor.Phase = phasor.ConfigFramePhase; + } + } + } + /// /// Retrieves type collection of . /// @@ -419,6 +526,22 @@ public static Dictionary GetLookupList(AdoDataConnection database, public static string Delete(AdoDataConnection database, int deviceID) => string.Empty; + private void UpdateStatusColor() + { + string statusColor; + if (m_acronym == OldAcronym) + statusColor = "green"; + else if (HasConflict) + statusColor = "red"; + else if (m_id != 0) + statusColor = "yellow"; + else + statusColor = "transparent"; + + if (statusColor != StatusColor) + StatusColor = statusColor; + } + #endregion } @@ -438,11 +561,14 @@ public class InputWizardDevicePhasor : DataModelBase private string m_baseKVInput; //private string m_destinationLabel; private bool m_include; + private bool m_useConfigLabels; internal string DatabaseLabel; internal string ConfigFrameLabel; internal string DatabaseType; internal string ConfigFrameType; + internal string DatabasePhase; + internal string ConfigFramePhase; #endregion @@ -460,6 +586,11 @@ public string Label { m_label = value is null || value.Length <= 200 ? value : value.Substring(0, 200); OnPropertyChanged("Label"); + + if (UseConfigLabels) + ConfigFrameLabel = value; + else + DatabaseLabel = value; } } @@ -475,6 +606,11 @@ public string Type { m_type = value; OnPropertyChanged("Type"); + + if (UseConfigLabels) + ConfigFrameType = value; + else + DatabaseType = value; } } @@ -579,6 +715,18 @@ public bool Include /// public float AngleAdder { get; set; } + internal bool UseConfigLabels + { + get => m_useConfigLabels; + set + { + m_useConfigLabels = value; + Label = m_useConfigLabels ? ConfigFrameLabel : DatabaseLabel; + Type = m_useConfigLabels ? ConfigFrameType : DatabaseType; + Phase = m_useConfigLabels ? ConfigFramePhase : DatabasePhase; + } + } + #endregion } } diff --git a/Source/Libraries/GSF.PhasorProtocols/UI/DataModels/Phasor.cs b/Source/Libraries/GSF.PhasorProtocols/UI/DataModels/Phasor.cs index 96855564e71..8494276e11d 100755 --- a/Source/Libraries/GSF.PhasorProtocols/UI/DataModels/Phasor.cs +++ b/Source/Libraries/GSF.PhasorProtocols/UI/DataModels/Phasor.cs @@ -432,6 +432,8 @@ public static string SaveAndReorder(AdoDataConnection database, Phasor phasor, i if (phasor.SourceIndex == 0) phasor.SourceIndex = database.ExecuteScalar("SELECT MAX(SourceIndex) FROM Phasor WHERE DeviceID = {0}", phasor.DeviceID) + 1; + else if (phasor.SourceIndex != oldSourceIndex) + database.ExecuteNonQuery("UPDATE Phasor SET SourceIndex = -SourceIndex WHERE DeviceID = {0} AND SourceIndex = {1}", phasor.DeviceID, phasor.SourceIndex); // Since phasors could be reordered in the source device, this test could inadvertently throw an exception when it should not - so the validation has been removed //if (database.ExecuteScalar("SELECT COUNT(*) FROM Phasor WHERE ID <> {0} AND DeviceID = {1} AND SourceIndex = {2}", phasor.ID, phasor.DeviceID, phasor.SourceIndex) > 0) diff --git a/Source/Libraries/GSF.PhasorProtocols/UI/WPF/UserControls/InputWizardUserControl.xaml b/Source/Libraries/GSF.PhasorProtocols/UI/WPF/UserControls/InputWizardUserControl.xaml index 1d2c148617e..f630c923d3c 100755 --- a/Source/Libraries/GSF.PhasorProtocols/UI/WPF/UserControls/InputWizardUserControl.xaml +++ b/Source/Libraries/GSF.PhasorProtocols/UI/WPF/UserControls/InputWizardUserControl.xaml @@ -249,10 +249,12 @@ - + - - + + + + @@ -276,13 +278,28 @@ + + + + + + + + + Visibility="{Binding Path=IsHighlighted, Converter={StaticResource ObjectToVisibilityConverter}}"/> diff --git a/Source/Libraries/GSF.PhasorProtocols/UI/WPF/ViewModels/InputWizardDevices.cs b/Source/Libraries/GSF.PhasorProtocols/UI/WPF/ViewModels/InputWizardDevices.cs index 13c9849717b..30deea3af2e 100755 --- a/Source/Libraries/GSF.PhasorProtocols/UI/WPF/ViewModels/InputWizardDevices.cs +++ b/Source/Libraries/GSF.PhasorProtocols/UI/WPF/ViewModels/InputWizardDevices.cs @@ -82,6 +82,11 @@ public DeviceMapping(Device device, IConfigurationCell cell, double ordinalDista StationAcronym = cell.StationName?.Replace(" ", "_").Replace("'", "").ToUpper(); LabelMatchValue = device.Acronym.Equals(StationLabel, StringComparison.Ordinal) ? 1.0D : 0.0D; MatchValue = ComputeMatchValue(); + + // The minimum Levenshtein distance has to be at least one + // because we already checked if the strings are equal + MinLabelDistance = new(() => Math.Max(1.0D, Device.Acronym.LevenshteinDistanceLowerBounds(StationLabel))); + MaxLabelDistance = new(() => Math.Max(1.0D, Device.Acronym.LevenshteinDistanceUpperBounds(StationLabel))); } #endregion @@ -94,9 +99,8 @@ public DeviceMapping(Device device, IConfigurationCell cell, double ordinalDista // A match value below 1 indicates the // mapping should probably be disregarded - public bool HasLowConfidence => - (CanBeUpdated && MatchValue < 1.0D - InverseSquared(MinLabelDistance)) || - (!CanBeUpdated && MatchValue < 1.0D); + public bool HasLowConfidence => MaxMatchValue < 1.0D; + public bool MaybeLowConfidence => MinMatchValue < 1.0D; private string LabelPrefix { get; } private string StationAcronym { get; } @@ -104,15 +108,21 @@ public DeviceMapping(Device device, IConfigurationCell cell, double ordinalDista private double LabelMatchValue { get; set; } private double MatchValue { get; set; } + private double MinMatchValue => CanBeUpdated + ? MatchValue + InverseSquared(MaxLabelDistance.Value) + : MatchValue; + + private double MaxMatchValue => CanBeUpdated + ? MatchValue + InverseSquared(MinLabelDistance.Value) + : MatchValue; + private string StationLabel => Device.Acronym.StartsWith(LabelPrefix, StringComparison.Ordinal) ? $"{LabelPrefix}{StationAcronym}" : StationAcronym; - // The minimum Levenshtein distance has to be at least one - // because we already checked if the strings are equal - private double MinLabelDistance => - Math.Max(1.0D, Device.Acronym.LevenshteinDistanceLowerBounds(StationLabel)); + private Lazy MinLabelDistance { get; } + private Lazy MaxLabelDistance { get; } // Indicates whether computing Levenshtein distance could change the ranking private bool CanBeUpdated => @@ -130,8 +140,7 @@ public bool CanBeOutrankedBy(DeviceMapping mapping) { return MatchValue < 3.0D && - mapping.CanBeUpdated && - MatchValue - mapping.MatchValue < InverseSquared(1.0D); + MinMatchValue < mapping.MaxMatchValue; } // Levenshtein distance can be expensive to compute @@ -246,6 +255,16 @@ public DeviceMapping Dequeue() } } + // Edge case for mappings that + // straddle the low-confidence threshold + if (mapping.MaybeLowConfidence) + { + mapping.UpdateRank(); + + if (mapping.HasLowConfidence) + return null; + } + MappedDevices.Add(mapping.Device); MappedCells.Add(mapping.Cell); return mapping; @@ -318,6 +337,7 @@ private static IEnumerable BuildInitialMappings(IEnumerable public ICommand ManualConfigurationCommand => m_manualConfigurationCommand ??= new RelayCommand(ManualConfiguration, () => CanSave); + /// + /// Gets to unlink from database record. + /// + public ICommand UnlinkCommand => m_unlinkCommand ??= new RelayCommand(UnlinkDevice); + /// /// Gets or sets summary message to be displayed on UI after parsing configuration file or frame. /// @@ -1235,11 +1251,13 @@ private void ParseConfiguration(bool displayPopup = true) string stationName = CultureInfo.CurrentUICulture.TextInfo.ToTitleCase(cell.StationName?.ToLower() ?? stationAcronym); string deviceAcronym = i < DeviceAcronyms.Length ? DeviceAcronyms[i] : (existingDevice?.Acronym ?? stationAcronym); int deviceID = existingDevice?.ID ?? 0; + Guid? globalID3 = null; Guid? uniqueID = null; decimal? longitude = null, latitude = null; if (cell is ConfigurationCell3 configCell3) { + globalID3 = configCell3.GlobalID; uniqueID = configCell3.GlobalID; longitude = configCell3.LongitudeM; latitude = configCell3.LatitudeM; @@ -1260,6 +1278,7 @@ private void ParseConfiguration(bool displayPopup = true) } finally { + globalID3 = null; uniqueID = null; } } @@ -1446,11 +1465,9 @@ string guessBaseKV(string baseKV, string phasorLabel, string deviceLabel) string getConfigFrameType(IPhasorDefinition phasor) => phasor.PhasorType == PhasorType.Current ? "I" : "V"; - string getPhasorLabel(IPhasorDefinition phasor) => UseConfigLabels ? getConfigFrameLabel(phasor) : getDatabaseLabel(phasor); - - string getPhasorType(IPhasorDefinition phasor) => UseConfigLabels ? getConfigFrameType(phasor) : getDatabaseType(phasor); + string getDatabasePhase(IPhasorDefinition phasor) => phasorExists(phasor) ? existingPhasors?[phasor.Index].Phase : getConfigFramePhase(phasor); - string getPhasorPhase(IPhasorDefinition phasor) + string getConfigFramePhase(IPhasorDefinition phasor) { string configPhase = string.Empty; @@ -1485,9 +1502,15 @@ string getPhasorPhase(IPhasorDefinition phasor) } } - return guessPhase(phasorExists(phasor) ? existingPhasors?[phasor.Index].Phase : configPhase, phasor.Label); + return guessPhase(configPhase, phasor.Label); } + string getPhasorLabel(IPhasorDefinition phasor) => UseConfigLabels ? getConfigFrameLabel(phasor) : getDatabaseLabel(phasor); + + string getPhasorType(IPhasorDefinition phasor) => UseConfigLabels ? getConfigFrameType(phasor) : getDatabaseType(phasor); + + string getPhasorPhase(IPhasorDefinition phasor) => UseConfigLabels ? getConfigFramePhase(phasor) : getDatabasePhase(phasor); + string getPhasorBaseKV(IPhasorDefinition phasor) { string configBaseKV = "0"; @@ -1511,15 +1534,24 @@ Tuple[] getAnalogScalars(AnalogDefinitionCollection analogs) => analogs.Select(getAnalogScalarSet).ToArray(); string deviceIndex = m_configurationFrame.Cells.Count > 1 ? $" {i + 1:N0}" : ""; + string databaseAcronym = string.IsNullOrWhiteSpace(existingDevice?.Acronym) ? deviceAcronym : existingDevice.Acronym; + string databaseName = string.IsNullOrWhiteSpace(existingDevice?.Name) ? stationName : existingDevice.Name; wizardDeviceList.Add(new InputWizardDevice { ID = deviceID, UniqueID = existingDevice?.UniqueID ?? uniqueID, - Acronym = string.IsNullOrWhiteSpace(existingDevice?.Acronym) ? deviceAcronym : existingDevice.Acronym, - Name = string.IsNullOrWhiteSpace(existingDevice?.Name) ? stationName : existingDevice.Name, - ConfigAcronym = $"Device{deviceIndex} label from config: {deviceAcronym}{(string.IsNullOrWhiteSpace(cell.IDLabel) ? "" : $" ({cell.IDLabel})")}", + GlobalID3 = globalID3, + OldAcronym = existingDevice?.Acronym, + Acronym = UseConfigLabels ? stationAcronym : databaseAcronym, + DatabaseAcronym = databaseAcronym, + ConfigFrameAcronym = stationAcronym, + Name = UseConfigLabels ? stationName : databaseName, + DatabaseName = databaseName, + ConfigFrameName = stationName, + ConfigAcronym = $"Device{deviceIndex} label from config: {stationAcronym}{(string.IsNullOrWhiteSpace(cell.IDLabel) ? "" : $" ({cell.IDLabel})")}", ConfigName = $"Device{deviceIndex} name derived from config: {stationName}", + LinkAcronym = $"Unlink from {deviceAcronym} in database", Longitude = existingDevice?.Longitude ?? longitude ?? -98.6m, Latitude = existingDevice?.Latitude ?? latitude ?? 37.5m, VendorDeviceID = existingDevice?.VendorDeviceID, @@ -1530,7 +1562,6 @@ Tuple[] getAnalogScalars(AnalogDefinitionCollection analogs) => AnalogCount = cell.AnalogDefinitions.Count, AddDigitals = cell.DigitalDefinitions.Count > 0, AddAnalogs = cell.AnalogDefinitions.Count > 0, - Existing = existingDevice is not null, DigitalLabels = GetAnalogOrDigitalLabels(cell.DigitalDefinitions), AnalogLabels = GetAnalogOrDigitalLabels(cell.AnalogDefinitions), AnalogScalars = getAnalogScalars(cell.AnalogDefinitions), @@ -1548,12 +1579,16 @@ from phasor in cell.PhasorDefinitions ConfigLabel = $"Phasor {phasor.Index + 1:N0} label from config: {phasor.Label}", ConfigType = $"Phasor {phasor.Index + 1:N0} type from config: {phasor.PhasorType}", Phase = getPhasorPhase(phasor), + DatabasePhase = getDatabasePhase(phasor), + ConfigFramePhase = getConfigFramePhase(phasor), BaseKVInput = getPhasorBaseKV(phasor), Include = true, MagnitudeMultiplier = getMagnitudeMultiplier(phasor), - AngleAdder = getAngleAdder(phasor) + AngleAdder = getAngleAdder(phasor), + UseConfigLabels = UseConfigLabels } - ).ToList()) + ).ToList()), + UseConfigLabels = UseConfigLabels }); } @@ -1892,6 +1927,12 @@ private void ManualConfiguration() cc.ShowDialog(); } + private void UnlinkDevice(object context) + { + if (context is InputWizardDevice device) + device.Unlink(); + } + /// /// Handles ReceivedServiceUpdate event. /// @@ -1998,9 +2039,7 @@ public void SaveConfiguration() if (inputWizardDevice.ID > 0) device = Device.GetDevice(database, $"WHERE ID = {inputWizardDevice.ID}"); - if (device is null) - device = Device.GetDevice(database, $"WHERE Acronym = '{inputWizardDevice.Acronym.ToUpper()}' AND NodeID = '{database.CurrentNodeID()}'"); - else + if (device is not null) device.Acronym = inputWizardDevice.Acronym.ToUpper(); bool newDevice = false; diff --git a/Source/Libraries/GSF.TimeSeries/UI/GSF.TimeSeries.UI.csproj b/Source/Libraries/GSF.TimeSeries/UI/GSF.TimeSeries.UI.csproj index ad3d4248eae..b74922a77a8 100755 --- a/Source/Libraries/GSF.TimeSeries/UI/GSF.TimeSeries.UI.csproj +++ b/Source/Libraries/GSF.TimeSeries/UI/GSF.TimeSeries.UI.csproj @@ -168,6 +168,9 @@ + + + diff --git a/Source/Libraries/GSF.TimeSeries/UI/Images/Unlink.png b/Source/Libraries/GSF.TimeSeries/UI/Images/Unlink.png new file mode 100644 index 00000000000..8013fd4bb8c Binary files /dev/null and b/Source/Libraries/GSF.TimeSeries/UI/Images/Unlink.png differ