diff --git a/src/MedWNetworkSim.App/NodeEditorWindow.xaml b/src/MedWNetworkSim.App/NodeEditorWindow.xaml index 7bf3cd4..2c7d2de 100644 --- a/src/MedWNetworkSim.App/NodeEditorWindow.xaml +++ b/src/MedWNetworkSim.App/NodeEditorWindow.xaml @@ -223,7 +223,7 @@ + SelectedItem="{Binding SelectedNodeTrafficType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> + ItemsSource="{Binding SelectedNodeRoleOptions}" + SelectedItem="{Binding SelectedNodeRoleName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> + IsEnabled="{Binding IsSelectedNodeProducer}" + Text="{Binding SelectedNodeProduction, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> + IsEnabled="{Binding IsSelectedNodeConsumer}" + Text="{Binding SelectedNodeConsumption, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> + Text="{Binding SelectedNodeTrafficSelectionLabel}" /> + Text="{Binding SelectedNodeTrafficRoleSummary}" /> diff --git a/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs b/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs index 8df0473..4e13c96 100644 --- a/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs +++ b/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs @@ -25,6 +25,7 @@ public sealed class MainWindowViewModel : ObservableObject private NodeTrafficProfileViewModel? selectedNodeTrafficProfile; private EdgeViewModel? selectedEdge; private TrafficTypeDefinitionEditorViewModel? selectedTrafficDefinition; + private bool isNormalizingNodeTrafficProfiles; private double workspaceWidth = 1600d; private double workspaceHeight = 1000d; private bool hasNetwork; @@ -149,13 +150,122 @@ public NodeViewModel? SelectedNode public NodeTrafficProfileViewModel? SelectedNodeTrafficProfile { get => selectedNodeTrafficProfile; - set => SetProperty(ref selectedNodeTrafficProfile, value); + set + { + if (ReferenceEquals(selectedNodeTrafficProfile, value)) + { + return; + } + + if (selectedNodeTrafficProfile is not null) + { + selectedNodeTrafficProfile.PropertyChanged -= HandleSelectedNodeTrafficProfilePropertyChanged; + } + + if (!SetProperty(ref selectedNodeTrafficProfile, value)) + { + return; + } + + if (selectedNodeTrafficProfile is not null) + { + selectedNodeTrafficProfile.PropertyChanged += HandleSelectedNodeTrafficProfilePropertyChanged; + } + + RaiseSelectedNodeTrafficEditorPropertiesChanged(); + } } public string SelectedNodeTrafficRoleHeadline => SelectedNode is null ? "Traffic Roles" : $"Traffic Roles For {SelectedNode.Name}"; + public IReadOnlyList SelectedNodeRoleOptions => SelectedNodeTrafficProfile?.RoleOptions ?? []; + + public string? SelectedNodeTrafficType + { + get => SelectedNodeTrafficProfile?.TrafficType; + set + { + if (SelectedNodeTrafficProfile is null || string.IsNullOrWhiteSpace(value)) + { + return; + } + + if (Comparer.Equals(SelectedNodeTrafficProfile.TrafficType, value)) + { + return; + } + + SelectedNodeTrafficProfile.TrafficType = value; + } + } + + public string? SelectedNodeRoleName + { + get => SelectedNodeTrafficProfile?.SelectedRoleName; + set + { + if (SelectedNodeTrafficProfile is null || string.IsNullOrWhiteSpace(value)) + { + return; + } + + if (string.Equals(SelectedNodeTrafficProfile.SelectedRoleName, value, StringComparison.Ordinal)) + { + return; + } + + SelectedNodeTrafficProfile.SelectedRoleName = value; + } + } + + public bool IsSelectedNodeProducer => SelectedNodeTrafficProfile?.IsProducer ?? false; + + public bool IsSelectedNodeConsumer => SelectedNodeTrafficProfile?.IsConsumer ?? false; + + public double SelectedNodeProduction + { + get => SelectedNodeTrafficProfile?.Production ?? 0d; + set + { + if (SelectedNodeTrafficProfile is null) + { + return; + } + + if (Math.Abs(SelectedNodeTrafficProfile.Production - value) < 0.000001d) + { + return; + } + + SelectedNodeTrafficProfile.Production = value; + } + } + + public double SelectedNodeConsumption + { + get => SelectedNodeTrafficProfile?.Consumption ?? 0d; + set + { + if (SelectedNodeTrafficProfile is null) + { + return; + } + + if (Math.Abs(SelectedNodeTrafficProfile.Consumption - value) < 0.000001d) + { + return; + } + + SelectedNodeTrafficProfile.Consumption = value; + } + } + + public string SelectedNodeTrafficSelectionLabel => SelectedNodeTrafficProfile?.SelectionLabel ?? "No traffic role selected"; + + public string SelectedNodeTrafficRoleSummary => SelectedNodeTrafficProfile?.RoleSummary ?? "Choose or add a traffic role entry."; + public EdgeViewModel? SelectedEdge { get => selectedEdge; @@ -268,8 +378,30 @@ public void AutoArrangeNodes() return; } - var arranged = fileService.AutoArrange(BuildValidatedNetwork()); - LoadNetwork(arranged, ActiveFileLabel, "Auto-arranged all node positions."); + var current = BuildValidatedNetwork(); + var arranged = fileService.AutoArrange(current); + var arrangedNodesById = arranged.Nodes.ToDictionary(node => node.Id, Comparer); + + foreach (var node in Nodes) + { + if (!arrangedNodesById.TryGetValue(node.Id, out var arrangedNode)) + { + continue; + } + + if (arrangedNode.X.HasValue) + { + node.X = arrangedNode.X.Value; + } + + if (arrangedNode.Y.HasValue) + { + node.Y = arrangedNode.Y.Value; + } + } + + RecalculateWorkspace(); + StatusMessage = "Auto-arranged all node positions."; } public void AddTrafficDefinition() @@ -630,6 +762,16 @@ private void HandleNodePositionChanged(object? sender, EventArgs e) private void HandleNodeDefinitionChanged(object? sender, EventArgs e) { + if (isNormalizingNodeTrafficProfiles) + { + return; + } + + if (!isNormalizingNodeTrafficProfiles && sender is NodeViewModel node) + { + NormalizeNodeTrafficProfiles(node); + } + OnPropertyChanged(nameof(SelectedNodeTrafficRoleHeadline)); RefreshDerivedStateAfterStructureChange("Updated node data."); } @@ -657,6 +799,11 @@ private void HandleEdgeDefinitionChanged(object? sender, EventArgs e) RefreshDerivedStateAfterStructureChange("Updated edge data."); } + private void HandleSelectedNodeTrafficProfilePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + RaiseSelectedNodeTrafficEditorPropertiesChanged(); + } + private void HandleTrafficDefinitionPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(TrafficTypeDefinitionEditorViewModel.Name)) @@ -796,6 +943,41 @@ private IEnumerable GetOrderedTrafficNames() return orderedNames; } + private void NormalizeNodeTrafficProfiles(NodeViewModel node) + { + isNormalizingNodeTrafficProfiles = true; + + try + { + foreach (var duplicateGroup in node.TrafficProfiles + .Where(profile => !string.IsNullOrWhiteSpace(profile.TrafficType)) + .GroupBy(profile => profile.TrafficType.Trim(), Comparer) + .Where(group => group.Count() > 1) + .ToList()) + { + var primaryProfile = duplicateGroup.First(); + + foreach (var duplicateProfile in duplicateGroup.Skip(1).ToList()) + { + primaryProfile.Production += duplicateProfile.Production; + primaryProfile.Consumption += duplicateProfile.Consumption; + primaryProfile.CanTransship |= duplicateProfile.CanTransship; + + if (ReferenceEquals(SelectedNodeTrafficProfile, duplicateProfile)) + { + SelectedNodeTrafficProfile = primaryProfile; + } + + node.RemoveTrafficProfile(duplicateProfile); + } + } + } + finally + { + isNormalizingNodeTrafficProfiles = false; + } + } + private void RecalculateWorkspace() { if (Nodes.Count == 0) @@ -886,4 +1068,17 @@ private static string GetNextUniqueName(string prefix, IEnumerable exist index++; } } + + private void RaiseSelectedNodeTrafficEditorPropertiesChanged() + { + OnPropertyChanged(nameof(SelectedNodeRoleOptions)); + OnPropertyChanged(nameof(SelectedNodeTrafficType)); + OnPropertyChanged(nameof(SelectedNodeRoleName)); + OnPropertyChanged(nameof(IsSelectedNodeProducer)); + OnPropertyChanged(nameof(IsSelectedNodeConsumer)); + OnPropertyChanged(nameof(SelectedNodeProduction)); + OnPropertyChanged(nameof(SelectedNodeConsumption)); + OnPropertyChanged(nameof(SelectedNodeTrafficSelectionLabel)); + OnPropertyChanged(nameof(SelectedNodeTrafficRoleSummary)); + } }