From c0666278dae39238452fe304e464a072641c337c Mon Sep 17 00:00:00 2001 From: wnj00524 <68168066+wnj00524@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:33:22 +0100 Subject: [PATCH] Fix traffic role editing and network validation --- .../Services/NetworkFileService.cs | 59 ++++++++--- .../ViewModels/MainWindowViewModel.cs | 97 +++++++++++++++---- 2 files changed, 124 insertions(+), 32 deletions(-) diff --git a/src/MedWNetworkSim.App/Services/NetworkFileService.cs b/src/MedWNetworkSim.App/Services/NetworkFileService.cs index e38c213..36c7a85 100644 --- a/src/MedWNetworkSim.App/Services/NetworkFileService.cs +++ b/src/MedWNetworkSim.App/Services/NetworkFileService.cs @@ -92,7 +92,7 @@ private NetworkModel NormalizeAndValidate(NetworkModel model, bool forceLayoutAl Name = string.IsNullOrWhiteSpace(node.Name) ? nodeId : node.Name.Trim(), X = node.X, Y = node.Y, - TrafficProfiles = NormalizeProfiles(node.TrafficProfiles) + TrafficProfiles = NormalizeProfiles(node.TrafficProfiles, nodeId) }); } @@ -129,6 +129,16 @@ private NetworkModel NormalizeAndValidate(NetworkModel model, bool forceLayoutAl throw new InvalidOperationException($"Duplicate edge id '{edgeId}' was found."); } + if (double.IsNaN(edge.Time) || double.IsInfinity(edge.Time) || edge.Time < 0d) + { + throw new InvalidOperationException($"Edge '{edgeId}' has an invalid time value. Use a finite number >= 0."); + } + + if (double.IsNaN(edge.Cost) || double.IsInfinity(edge.Cost) || edge.Cost < 0d) + { + throw new InvalidOperationException($"Edge '{edgeId}' has an invalid cost value. Use a finite number >= 0."); + } + if (capacity.HasValue && (double.IsNaN(capacity.Value) || double.IsInfinity(capacity.Value) || capacity.Value < 0d)) { throw new InvalidOperationException($"Edge '{edgeId}' has an invalid capacity value. Use a number >= 0 or omit the property for unlimited capacity."); @@ -159,19 +169,44 @@ private NetworkModel NormalizeAndValidate(NetworkModel model, bool forceLayoutAl }; } - private static List NormalizeProfiles(IEnumerable? profiles) + private static List NormalizeProfiles(IEnumerable? profiles, string nodeId) { - // Duplicate traffic rows on the same node are collapsed into one persisted profile per traffic type. - return (profiles ?? []) - .Where(profile => !string.IsNullOrWhiteSpace(profile.TrafficType)) - .GroupBy(profile => profile.TrafficType.Trim(), Comparer) - .Select(group => new NodeTrafficProfile + var normalizedProfiles = new Dictionary(Comparer); + + foreach (var profile in profiles ?? []) + { + if (string.IsNullOrWhiteSpace(profile.TrafficType)) + { + throw new InvalidOperationException($"Node '{nodeId}' contains a traffic profile with a blank trafficType."); + } + + if (double.IsNaN(profile.Production) || double.IsInfinity(profile.Production) || profile.Production < 0d) { - TrafficType = group.Key, - Production = group.Sum(profile => profile.Production), - Consumption = group.Sum(profile => profile.Consumption), - CanTransship = group.Any(profile => profile.CanTransship) - }) + throw new InvalidOperationException($"Node '{nodeId}' has an invalid production value for traffic '{profile.TrafficType}'. Use a finite number >= 0."); + } + + if (double.IsNaN(profile.Consumption) || double.IsInfinity(profile.Consumption) || profile.Consumption < 0d) + { + throw new InvalidOperationException($"Node '{nodeId}' has an invalid consumption value for traffic '{profile.TrafficType}'. Use a finite number >= 0."); + } + + var trafficType = profile.TrafficType.Trim(); + if (!normalizedProfiles.TryGetValue(trafficType, out var normalizedProfile)) + { + normalizedProfile = new NodeTrafficProfile + { + TrafficType = trafficType + }; + normalizedProfiles[trafficType] = normalizedProfile; + } + + normalizedProfile.Production += profile.Production; + normalizedProfile.Consumption += profile.Consumption; + normalizedProfile.CanTransship |= profile.CanTransship; + } + + // Duplicate traffic rows on the same node are collapsed into one persisted profile per traffic type. + return normalizedProfiles.Values .OrderBy(profile => profile.TrafficType, Comparer) .ToList(); } diff --git a/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs b/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs index 1f01b55..0384d51 100644 --- a/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs +++ b/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs @@ -26,6 +26,7 @@ public sealed class MainWindowViewModel : ObservableObject private EdgeViewModel? selectedEdge; private TrafficTypeDefinitionEditorViewModel? selectedTrafficDefinition; private bool isNormalizingNodeTrafficProfiles; + private bool isAdjustingTrafficDefinitionNames; private double workspaceWidth = 1600d; private double workspaceHeight = 1000d; private bool hasNetwork; @@ -410,14 +411,7 @@ public void AddTrafficDefinition() { EnsureNetworkExists(); - var definition = new TrafficTypeDefinitionEditorViewModel(new TrafficTypeDefinition - { - Name = GetNextUniqueName("Traffic", TrafficDefinitions.Select(item => item.Name)), - RoutingPreference = RoutingPreference.TotalCost - }); - - RegisterTrafficDefinition(definition); - SelectedTrafficDefinition = definition; + CreateTrafficDefinition(); RefreshDerivedStateAfterStructureChange("Added a new traffic type."); } @@ -499,6 +493,14 @@ public void AddTrafficProfileToSelectedNode() .Where(name => !string.IsNullOrWhiteSpace(name)) .FirstOrDefault(name => SelectedNode.TrafficProfiles.All(profile => !Comparer.Equals(profile.TrafficType, name))) ?? primaryTrafficDefinition.Name; + + var createdTrafficDefinition = false; + if (SelectedNode.TrafficProfiles.Any(profile => Comparer.Equals(profile.TrafficType, trafficName))) + { + trafficName = CreateTrafficDefinition().Name; + createdTrafficDefinition = true; + } + var profile = new NodeTrafficProfileViewModel(new NodeTrafficProfile { TrafficType = trafficName @@ -506,7 +508,10 @@ public void AddTrafficProfileToSelectedNode() SelectedNode.AddTrafficProfile(profile); SelectedNodeTrafficProfile = profile; - RefreshDerivedStateAfterStructureChange("Added a traffic profile to the selected node."); + RefreshDerivedStateAfterStructureChange( + createdTrafficDefinition + ? "Added a traffic profile to the selected node and created a new traffic type for it." + : "Added a traffic profile to the selected node."); } public void RemoveSelectedTrafficProfileFromNode() @@ -585,15 +590,7 @@ private TrafficTypeDefinitionEditorViewModel EnsurePrimaryTrafficDefinition() return existingDefinition; } - var definition = new TrafficTypeDefinitionEditorViewModel(new TrafficTypeDefinition - { - Name = GetNextUniqueName("Traffic", TrafficDefinitions.Select(item => item.Name)), - RoutingPreference = RoutingPreference.TotalCost - }); - - RegisterTrafficDefinition(definition); - SelectedTrafficDefinition = definition; - return definition; + return CreateTrafficDefinition(); } private void LoadBundledSampleIfAvailable() @@ -819,11 +816,44 @@ private void HandleTrafficDefinitionPropertyChanged(object? sender, PropertyChan private void HandleTrafficDefinitionNameChanged(object? sender, ValueChangedEventArgs e) { + if (isAdjustingTrafficDefinitionNames || sender is not TrafficTypeDefinitionEditorViewModel definition) + { + return; + } + + var oldValue = e.OldValue?.Trim() ?? string.Empty; + var requestedName = definition.Name; + var normalizedName = requestedName.Trim(); + + if (string.IsNullOrWhiteSpace(normalizedName)) + { + RestoreTrafficDefinitionName(definition, oldValue); + StatusMessage = "Traffic type names cannot be blank."; + return; + } + + if (TrafficDefinitions.Any(other => !ReferenceEquals(other, definition) && Comparer.Equals(other.Name, normalizedName))) + { + RestoreTrafficDefinitionName(definition, oldValue); + StatusMessage = $"Traffic type '{normalizedName}' already exists."; + return; + } + + if (!string.Equals(requestedName, normalizedName, StringComparison.Ordinal)) + { + RestoreTrafficDefinitionName(definition, normalizedName); + } + + if (string.Equals(oldValue, normalizedName, StringComparison.Ordinal)) + { + return; + } + foreach (var node in Nodes) { - foreach (var profile in node.TrafficProfiles.Where(profile => Comparer.Equals(profile.TrafficType, e.OldValue))) + foreach (var profile in node.TrafficProfiles.Where(profile => Comparer.Equals(profile.TrafficType, oldValue))) { - profile.TrafficType = e.NewValue; + profile.TrafficType = normalizedName; } } @@ -1073,6 +1103,33 @@ private static string GetNextUniqueName(string prefix, IEnumerable exist } } + private TrafficTypeDefinitionEditorViewModel CreateTrafficDefinition() + { + var definition = new TrafficTypeDefinitionEditorViewModel(new TrafficTypeDefinition + { + Name = GetNextUniqueName("Traffic", TrafficDefinitions.Select(item => item.Name)), + RoutingPreference = RoutingPreference.TotalCost + }); + + RegisterTrafficDefinition(definition); + SelectedTrafficDefinition = definition; + return definition; + } + + private void RestoreTrafficDefinitionName(TrafficTypeDefinitionEditorViewModel definition, string name) + { + isAdjustingTrafficDefinitionNames = true; + + try + { + definition.Name = name; + } + finally + { + isAdjustingTrafficDefinitionNames = false; + } + } + private void RaiseSelectedNodeTrafficEditorPropertiesChanged() { OnPropertyChanged(nameof(SelectedNodeRoleOptions));