Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions src/MedWNetworkSim.App/Services/NetworkFileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
});
}

Expand Down Expand Up @@ -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.");
Expand Down Expand Up @@ -159,19 +169,44 @@ private NetworkModel NormalizeAndValidate(NetworkModel model, bool forceLayoutAl
};
}

private static List<NodeTrafficProfile> NormalizeProfiles(IEnumerable<NodeTrafficProfile>? profiles)
private static List<NodeTrafficProfile> NormalizeProfiles(IEnumerable<NodeTrafficProfile>? 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<string, NodeTrafficProfile>(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();
}
Expand Down
97 changes: 77 additions & 20 deletions src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.");
}

Expand Down Expand Up @@ -499,14 +493,25 @@ 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
});

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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -819,11 +816,44 @@ private void HandleTrafficDefinitionPropertyChanged(object? sender, PropertyChan

private void HandleTrafficDefinitionNameChanged(object? sender, ValueChangedEventArgs<string> 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)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Iterating over node.TrafficProfiles while modifying its elements' properties can lead to an InvalidOperationException (Collection was modified) if the property change triggers a profile normalization that removes items from the collection. Since profile.TrafficType = normalizedName can cause NormalizeNodeTrafficProfiles to run and potentially remove duplicate profiles, you should create a snapshot of the matching profiles before iterating.

            foreach (var profile in node.TrafficProfiles.Where(profile => Comparer.Equals(profile.TrafficType, oldValue)).ToList())

{
profile.TrafficType = e.NewValue;
profile.TrafficType = normalizedName;
}
}

Expand Down Expand Up @@ -1073,6 +1103,33 @@ private static string GetNextUniqueName(string prefix, IEnumerable<string> 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));
Expand Down
Loading