diff --git a/Assets/SEE/Layout/NodeLayouts/IncrementalTreeMap/LocalMoves.cs b/Assets/SEE/Layout/NodeLayouts/IncrementalTreeMap/LocalMoves.cs
index 0fe772ec78..58c37c2adb 100644
--- a/Assets/SEE/Layout/NodeLayouts/IncrementalTreeMap/LocalMoves.cs
+++ b/Assets/SEE/Layout/NodeLayouts/IncrementalTreeMap/LocalMoves.cs
@@ -1 +1,297 @@
-using System;
using System.Collections.Generic;
using System.Linq;
using MathNet.Numerics.LinearAlgebra;
using SEE.Game.City;
using UnityEngine.Assertions;
using static SEE.Layout.NodeLayouts.IncrementalTreeMap.Direction;
namespace SEE.Layout.NodeLayouts.IncrementalTreeMap
{
///
/// Provides algorithms for adding and deleting nodes to a layout
/// and an algorithm to improve visual quality of a layout.
///
internal static class LocalMoves
{
///
/// Finds possible s for a specific segment.
/// Examples are flipping the segment or stretching a node over the segment.
///
/// the segment
/// List of s
private static IList FindLocalMoves(Segment segment)
{
List result = new List();
if (segment.IsConst)
{
return result;
}
if (segment.Side1Nodes.Count == 1 && segment.Side2Nodes.Count == 1)
{
result.Add(new FlipMove(segment.Side1Nodes.First(), segment.Side2Nodes.First(), true));
result.Add(new FlipMove(segment.Side1Nodes.First(), segment.Side2Nodes.First(), false));
return result;
}
if (segment.IsVertical)
{
Node upperNode1 = Utils.ArgMax(segment.Side1Nodes, x => x.Rectangle.Z);
Node upperNode2 = Utils.ArgMax(segment.Side2Nodes, x => x.Rectangle.Z);
Assert.IsTrue(upperNode1.SegmentsDictionary()[Upper] == upperNode2.SegmentsDictionary()[Upper]);
Node lowerNode1 = Utils.ArgMin(segment.Side1Nodes, x => x.Rectangle.Z);
Node lowerNode2 = Utils.ArgMin(segment.Side2Nodes, x => x.Rectangle.Z);
Assert.IsTrue(lowerNode1.SegmentsDictionary()[Lower] == lowerNode2.SegmentsDictionary()[Lower]);
result.Add(new StretchMove(upperNode1, upperNode2));
result.Add(new StretchMove(lowerNode1, lowerNode2));
return result;
}
Node rightNode1 = Utils.ArgMax(segment.Side1Nodes, x => x.Rectangle.X);
Node rightNode2 = Utils.ArgMax(segment.Side2Nodes, x => x.Rectangle.X);
Assert.IsTrue(rightNode1.SegmentsDictionary()[Right] == rightNode2.SegmentsDictionary()[Right]);
Node leftNode1 = Utils.ArgMin(segment.Side1Nodes, x => x.Rectangle.X);
Node leftNode2 = Utils.ArgMin(segment.Side2Nodes, x => x.Rectangle.X);
Assert.IsTrue(leftNode1.SegmentsDictionary()[Left] == leftNode2.SegmentsDictionary()[Left]);
result.Add(new StretchMove(rightNode1, rightNode2));
result.Add(new StretchMove(leftNode1, leftNode2));
return result;
}
///
/// Adds a node to the layout.
/// Will NOT add to the list of .
///
/// nodes that represent a layout
/// the node that should be added
public static void AddNode(IList nodes, Node newNode)
{
// node with rectangle with highest aspect ratio
Node bestNode = Utils.ArgMax(nodes, x => x.Rectangle.AspectRatio());
newNode.Rectangle = bestNode.Rectangle.Clone();
IDictionary segments = bestNode.SegmentsDictionary();
foreach (Direction dir in Enum.GetValues(typeof(Direction)))
{
newNode.RegisterSegment(segments[dir], dir);
}
if (bestNode.Rectangle.Width >= bestNode.Rectangle.Depth)
{
// [bestNode]|[newNode]
Segment newSegment = new Segment(isConst: false, isVertical: true);
newNode.RegisterSegment(newSegment, Left);
bestNode.RegisterSegment(newSegment, Right);
bestNode.Rectangle.Width *= 0.5f;
newNode.Rectangle.Width *= 0.5f;
newNode.Rectangle.X = bestNode.Rectangle.X + bestNode.Rectangle.Width;
}
else
{
// [newNode]
// ---------
// [bestNode]
Segment newSegment = new Segment(isConst: false, isVertical: false);
newNode.RegisterSegment(newSegment, Lower);
bestNode.RegisterSegment(newSegment, Upper);
bestNode.Rectangle.Depth *= 0.5f;
newNode.Rectangle.Depth *= 0.5f;
newNode.Rectangle.Z = bestNode.Rectangle.Z + bestNode.Rectangle.Depth;
}
}
///
/// Deletes a node from the layout.
///
/// node to be deleted, part of a layout
public static void DeleteNode(Node obsoleteNode)
{
// check whether node is grounded
IDictionary segments = obsoleteNode.SegmentsDictionary();
bool isGrounded = false;
if (segments[Left].Side2Nodes.Count == 1 && !segments[Left].IsConst)
{
isGrounded = true;
//[E][O]
Node[] expandingNodes = segments[Left].Side1Nodes.ToArray();
foreach (Node node in expandingNodes)
{
node.Rectangle.Width += obsoleteNode.Rectangle.Width;
node.RegisterSegment(segments[Right], Right);
}
}
else if (segments[Right].Side1Nodes.Count == 1 && !segments[Right].IsConst)
{
isGrounded = true;
//[O][E]
Node[] expandingNodes = segments[Right].Side2Nodes.ToArray();
foreach (Node node in expandingNodes)
{
node.Rectangle.X = obsoleteNode.Rectangle.X;
node.Rectangle.Width += obsoleteNode.Rectangle.Width;
node.RegisterSegment(segments[Left], Left);
}
}
else if (segments[Lower].Side2Nodes.Count == 1 && !segments[Lower].IsConst)
{
isGrounded = true;
//[O]
//[E]
Node[] expandingNodes = segments[Lower].Side1Nodes.ToArray();
foreach (Node node in expandingNodes)
{
node.Rectangle.Depth += obsoleteNode.Rectangle.Depth;
node.RegisterSegment(segments[Upper], Upper);
}
}
else if (segments[Upper].Side1Nodes.Count == 1 && !segments[Upper].IsConst)
{
isGrounded = true;
//[E]
//[O]
Node[] expandingNodes = segments[Upper].Side2Nodes.ToArray();
foreach (Node node in expandingNodes)
{
node.Rectangle.Z = obsoleteNode.Rectangle.Z;
node.Rectangle.Depth += obsoleteNode.Rectangle.Depth;
node.RegisterSegment(segments[Lower], Lower);
}
}
if (isGrounded)
{
foreach (Direction dir in Enum.GetValues(typeof(Direction)))
{
obsoleteNode.DeregisterSegment(dir);
}
}
else
{
Segment bestSegment = Utils.ArgMin(segments.Values, x => x.Side1Nodes.Count + x.Side2Nodes.Count);
IList moves = FindLocalMoves(bestSegment);
Assert.IsTrue(moves.All(x => x is (StretchMove)));
foreach (LocalMove move in moves)
{
if (move.Node1 != obsoleteNode && move.Node2 != obsoleteNode)
{
move.Apply();
DeleteNode(obsoleteNode);
return;
}
}
// We should never arrive here
Assert.IsFalse(true);
}
}
///
/// Searches the space of layouts that are similar to the layout of
/// (in terms of distance in local moves).
/// Apply the layout with the best visual quality to
///
/// nodes that represent a layout
/// settings for search
public static void LocalMovesSearch(List nodes, IncrementalTreeMapSetting settings)
{
List<(List nodes, double visualQuality, List movesList)> allResults = RecursiveMakeMoves(
nodes,
new List(),
settings);
allResults.Add((nodes, AspectRatiosPNorm(nodes, settings.PNorm), new List()));
List bestResult = Utils.ArgMin(allResults,
x => x.Item2 * 10 + x.Item3.Count).Item1;
IDictionary nodesDictionary = nodes.ToDictionary(n => n.ID, n => n);
foreach (Node resultNode in bestResult)
{
nodesDictionary[resultNode.ID].Rectangle = resultNode.Rectangle;
}
Utils.CloneSegments(from: bestResult, to: nodesDictionary);
}
///
/// Makes recursively local moves on clones of the layout to find similar layouts with good visual quality.
///
/// nodes that represent a layout
/// moves that are done before in recursion
/// the settings
/// selection of reached layouts, as tuples of nodes, visual quality measure of the layout and
/// the local moves that are applied to get this layout.
private static List<(List nodes, double visualQuality, List movesList)> RecursiveMakeMoves(
IList nodes,
IList movesUntilNow,
IncrementalTreeMapSetting settings)
{
List<(List nodes, double visualQuality, List movesList)> resultThisRecursion = new();
if (movesUntilNow.Count >= settings.localMovesDepth)
{
return resultThisRecursion;
}
ICollection relevantSegments;
if (movesUntilNow.Count == 0)
{
relevantSegments = nodes.SelectMany(n => n.SegmentsDictionary().Values).ToHashSet();
}
else
{
IEnumerable relevantNodes = movesUntilNow.SelectMany(m => new[] { m.Node1.ID, m.Node2.ID })
.Distinct()
.Select(id => nodes.First(n => n.ID == id));
relevantSegments = relevantNodes.SelectMany(n => n.SegmentsDictionary().Values).ToHashSet();
}
IEnumerable possibleMoves = relevantSegments.SelectMany(FindLocalMoves);
foreach (LocalMove move in possibleMoves)
{
IDictionary nodeClonesDictionary = Utils.CloneGraph(nodes);
List nodeClonesList = nodeClonesDictionary.Values.ToList();
LocalMove moveClone = move.Clone(nodeClonesDictionary);
moveClone.Apply();
bool works = CorrectAreas.Correct(nodeClonesList, settings);
if (!works)
{
continue;
}
List newMovesList = new List(movesUntilNow) { moveClone };
resultThisRecursion.Add(
(nodeClonesList, AspectRatiosPNorm(nodeClonesList, settings.PNorm), newMovesList));
}
resultThisRecursion.Sort((x, y) => x.Item2.CompareTo(y.Item2));
resultThisRecursion = resultThisRecursion.Take(settings.localMovesBranchingLimit).ToList();
List<(List nodes, double visualQuality, List movesList)> resultsNextRecursions = new();
foreach ((List resultNodes, double _, List resultMoves) in resultThisRecursion)
{
resultsNextRecursions.AddRange(RecursiveMakeMoves(resultNodes,resultMoves, settings));
}
return resultThisRecursion.Concat(resultsNextRecursions).ToList();
}
///
/// Measures the visual quality of a layout, based on the aspect ratio of .
///
/// The nodes the should be assessed.
/// Determines the specific norm.
/// A measure for the visual quality of the nodes.
/// Return value is greater than or equal to 1, while 1 means perfect visual quality
private static double AspectRatiosPNorm(IList nodes, double p)
{
Vector aspectRatios =
Vector.Build.DenseOfEnumerable(nodes.Select(n => n.Rectangle.AspectRatio()));
return aspectRatios.Norm(p);
}
}
}
\ No newline at end of file
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MathNet.Numerics.LinearAlgebra;
+using SEE.Game.City;
+using UnityEngine.Assertions;
+using static SEE.Layout.NodeLayouts.IncrementalTreeMap.Direction;
+
+namespace SEE.Layout.NodeLayouts.IncrementalTreeMap
+{
+ ///
+ /// Provides algorithms for adding and deleting nodes to a layout
+ /// and an algorithm to improve visual quality of a layout.
+ ///
+ internal static class LocalMoves
+ {
+ ///
+ /// Finds possible s for a specific segment.
+ /// Examples are flipping the segment or stretching a node over the segment.
+ ///
+ /// the segment
+ /// List of s
+ private static IList FindLocalMoves(Segment segment)
+ {
+ List result = new List();
+ if (segment.IsConst)
+ {
+ return result;
+ }
+
+ if (segment.Side1Nodes.Count == 1 && segment.Side2Nodes.Count == 1)
+ {
+ result.Add(new FlipMove(segment.Side1Nodes.First(), segment.Side2Nodes.First(), true));
+ result.Add(new FlipMove(segment.Side1Nodes.First(), segment.Side2Nodes.First(), false));
+ return result;
+ }
+
+ if (segment.IsVertical)
+ {
+ Node upperNode1 = Utils.ArgMax(segment.Side1Nodes, x => x.Rectangle.Z);
+ Node upperNode2 = Utils.ArgMax(segment.Side2Nodes, x => x.Rectangle.Z);
+ Assert.IsTrue(upperNode1.SegmentsDictionary()[Upper] == upperNode2.SegmentsDictionary()[Upper]);
+
+ Node lowerNode1 = Utils.ArgMin(segment.Side1Nodes, x => x.Rectangle.Z);
+ Node lowerNode2 = Utils.ArgMin(segment.Side2Nodes, x => x.Rectangle.Z);
+ Assert.IsTrue(lowerNode1.SegmentsDictionary()[Lower] == lowerNode2.SegmentsDictionary()[Lower]);
+
+ result.Add(new StretchMove(upperNode1, upperNode2));
+ result.Add(new StretchMove(lowerNode1, lowerNode2));
+ return result;
+ }
+
+ Node rightNode1 = Utils.ArgMax(segment.Side1Nodes, x => x.Rectangle.X);
+ Node rightNode2 = Utils.ArgMax(segment.Side2Nodes, x => x.Rectangle.X);
+ Assert.IsTrue(rightNode1.SegmentsDictionary()[Right] == rightNode2.SegmentsDictionary()[Right]);
+
+ Node leftNode1 = Utils.ArgMin(segment.Side1Nodes, x => x.Rectangle.X);
+ Node leftNode2 = Utils.ArgMin(segment.Side2Nodes, x => x.Rectangle.X);
+ Assert.IsTrue(leftNode1.SegmentsDictionary()[Left] == leftNode2.SegmentsDictionary()[Left]);
+
+ result.Add(new StretchMove(rightNode1, rightNode2));
+ result.Add(new StretchMove(leftNode1, leftNode2));
+ return result;
+ }
+
+ ///
+ /// Adds a node to the layout.
+ /// Will NOT add to the list of .
+ ///
+ /// nodes that represent a layout
+ /// the node that should be added
+ public static void AddNode(IList nodes, Node newNode)
+ {
+ // node with rectangle with highest aspect ratio
+ Node bestNode = Utils.ArgMax(nodes, x => x.Rectangle.AspectRatio());
+
+ newNode.Rectangle = bestNode.Rectangle.Clone();
+ IDictionary segments = bestNode.SegmentsDictionary();
+ foreach (Direction dir in Enum.GetValues(typeof(Direction)))
+ {
+ newNode.RegisterSegment(segments[dir], dir);
+ }
+
+ if (bestNode.Rectangle.Width >= bestNode.Rectangle.Depth)
+ {
+ // [bestNode]|[newNode]
+ Segment newSegment = new Segment(isConst: false, isVertical: true);
+ newNode.RegisterSegment(newSegment, Left);
+ bestNode.RegisterSegment(newSegment, Right);
+ bestNode.Rectangle.Width *= 0.5f;
+ newNode.Rectangle.Width *= 0.5f;
+ newNode.Rectangle.X = bestNode.Rectangle.X + bestNode.Rectangle.Width;
+ }
+ else
+ {
+ // [newNode]
+ // ---------
+ // [bestNode]
+ Segment newSegment = new Segment(isConst: false, isVertical: false);
+ newNode.RegisterSegment(newSegment, Lower);
+ bestNode.RegisterSegment(newSegment, Upper);
+ bestNode.Rectangle.Depth *= 0.5f;
+ newNode.Rectangle.Depth *= 0.5f;
+ newNode.Rectangle.Z = bestNode.Rectangle.Z + bestNode.Rectangle.Depth;
+ }
+ }
+
+ ///
+ /// Deletes a node from the layout.
+ ///
+ /// node to be deleted, part of a layout
+ public static void DeleteNode(Node obsoleteNode)
+ {
+ // check whether node is grounded
+ IDictionary segments = obsoleteNode.SegmentsDictionary();
+ bool isGrounded = false;
+ if (segments[Left].Side2Nodes.Count == 1 && !segments[Left].IsConst)
+ {
+ isGrounded = true;
+ //[E][O]
+ Node[] expandingNodes = segments[Left].Side1Nodes.ToArray();
+ foreach (Node node in expandingNodes)
+ {
+ node.Rectangle.Width += obsoleteNode.Rectangle.Width;
+ node.RegisterSegment(segments[Right], Right);
+ }
+ }
+ else if (segments[Right].Side1Nodes.Count == 1 && !segments[Right].IsConst)
+ {
+ isGrounded = true;
+ //[O][E]
+ Node[] expandingNodes = segments[Right].Side2Nodes.ToArray();
+ foreach (Node node in expandingNodes)
+ {
+ node.Rectangle.X = obsoleteNode.Rectangle.X;
+ node.Rectangle.Width += obsoleteNode.Rectangle.Width;
+ node.RegisterSegment(segments[Left], Left);
+ }
+ }
+ else if (segments[Lower].Side2Nodes.Count == 1 && !segments[Lower].IsConst)
+ {
+ isGrounded = true;
+ //[O]
+ //[E]
+ Node[] expandingNodes = segments[Lower].Side1Nodes.ToArray();
+ foreach (Node node in expandingNodes)
+ {
+ node.Rectangle.Depth += obsoleteNode.Rectangle.Depth;
+ node.RegisterSegment(segments[Upper], Upper);
+ }
+ }
+ else if (segments[Upper].Side1Nodes.Count == 1 && !segments[Upper].IsConst)
+ {
+ isGrounded = true;
+ //[E]
+ //[O]
+ Node[] expandingNodes = segments[Upper].Side2Nodes.ToArray();
+ foreach (Node node in expandingNodes)
+ {
+ node.Rectangle.Z = obsoleteNode.Rectangle.Z;
+ node.Rectangle.Depth += obsoleteNode.Rectangle.Depth;
+ node.RegisterSegment(segments[Lower], Lower);
+ }
+ }
+
+ if (isGrounded)
+ {
+ foreach (Direction dir in Enum.GetValues(typeof(Direction)))
+ {
+ obsoleteNode.DeregisterSegment(dir);
+ }
+ }
+ else
+ {
+ Segment bestSegment = Utils.ArgMin(segments.Values, x => x.Side1Nodes.Count + x.Side2Nodes.Count);
+
+ IList moves = FindLocalMoves(bestSegment);
+ Assert.IsTrue(moves.All(x => x is (StretchMove)));
+ foreach (LocalMove move in moves)
+ {
+ if (move.Node1 != obsoleteNode && move.Node2 != obsoleteNode)
+ {
+ move.Apply();
+ DeleteNode(obsoleteNode);
+ return;
+ }
+ }
+
+ // We should never arrive here
+ Assert.IsFalse(true);
+ }
+ }
+
+ ///
+ /// Searches the space of layouts that are similar to the layout of
+ /// (in terms of distance in local moves).
+ /// Apply the layout with the best visual quality to
+ ///
+ /// nodes that represent a layout
+ /// settings for search
+ public static void LocalMovesSearch(List nodes, IncrementalTreeMapSetting settings)
+ {
+ List<(List nodes, double visualQuality, List movesList)> allResults = RecursiveMakeMoves(
+ nodes,
+ new List(),
+ settings);
+ allResults.Add((nodes, AspectRatiosPNorm(nodes, settings.PNorm), new List()));
+ List bestResult = Utils.ArgMin(allResults,
+ x => x.Item2 * 10 + x.Item3.Count).Item1;
+
+ IDictionary nodesDictionary = nodes.ToDictionary(n => n.ID, n => n);
+ foreach (Node resultNode in bestResult)
+ {
+ nodesDictionary[resultNode.ID].Rectangle = resultNode.Rectangle;
+ }
+
+ Utils.CloneSegments(from: bestResult, to: nodesDictionary);
+ }
+
+ ///
+ /// Makes recursively local moves on clones of the layout to find similar layouts with good visual quality.
+ ///
+ /// nodes that represent a layout
+ /// moves that are done before in recursion
+ /// the settings
+ /// selection of reached layouts, as tuples of nodes, visual quality measure of the layout and
+ /// the local moves that are applied to get this layout.
+ private static List<(List nodes, double visualQuality, List movesList)> RecursiveMakeMoves(
+ IList nodes,
+ IList movesUntilNow,
+ IncrementalTreeMapSetting settings)
+ {
+ List<(List nodes, double visualQuality, List movesList)> resultThisRecursion = new();
+ if (movesUntilNow.Count >= settings.localMovesDepth)
+ {
+ return resultThisRecursion;
+ }
+
+ ICollection relevantSegments;
+ if (movesUntilNow.Count == 0)
+ {
+ relevantSegments = nodes.SelectMany(n => n.SegmentsDictionary().Values).ToHashSet();
+ }
+ else
+ {
+ IEnumerable relevantNodes = movesUntilNow.SelectMany(m => new[] { m.Node1.ID, m.Node2.ID })
+ .Distinct()
+ .Select(id => nodes.First(n => n.ID == id));
+ relevantSegments = relevantNodes.SelectMany(n => n.SegmentsDictionary().Values).ToHashSet();
+ }
+
+ IEnumerable possibleMoves = relevantSegments.SelectMany(FindLocalMoves);
+ foreach (LocalMove move in possibleMoves)
+ {
+ IDictionary nodeClonesDictionary = Utils.CloneGraph(nodes);
+ List nodeClonesList = nodeClonesDictionary.Values.ToList();
+ LocalMove moveClone = move.Clone(nodeClonesDictionary);
+
+ moveClone.Apply();
+ bool works = CorrectAreas.Correct(nodeClonesList, settings);
+ if (!works)
+ {
+ continue;
+ }
+
+ List newMovesList = new List(movesUntilNow) { moveClone };
+ resultThisRecursion.Add(
+ (nodeClonesList, AspectRatiosPNorm(nodeClonesList, settings.PNorm), newMovesList));
+ }
+
+ resultThisRecursion.Sort((x, y) => x.Item2.CompareTo(y.Item2));
+ resultThisRecursion = resultThisRecursion.Take(settings.localMovesBranchingLimit).ToList();
+
+ List<(List nodes, double visualQuality, List movesList)> resultsNextRecursions = new();
+ foreach ((List resultNodes, double _, List resultMoves) in resultThisRecursion)
+ {
+ resultsNextRecursions.AddRange(RecursiveMakeMoves(resultNodes,resultMoves, settings));
+ }
+
+ return resultThisRecursion.Concat(resultsNextRecursions).ToList();
+ }
+
+ ///
+ /// Measures the visual quality of a layout, based on the aspect ratio of .
+ ///
+ /// The nodes the should be assessed.
+ /// Determines the specific norm.
+ /// A measure for the visual quality of the nodes.
+ /// Return value is greater than or equal to 1, while 1 means perfect visual quality
+ private static double AspectRatiosPNorm(IList nodes, double p)
+ {
+ Vector aspectRatios =
+ Vector.Build.DenseOfEnumerable(nodes.Select(n => n.Rectangle.AspectRatio()));
+ return aspectRatios.Norm(p);
+ }
+ }
+}
diff --git a/Assets/SEE/Layout/NodeLayouts/IncrementalTreeMap/Utils.cs b/Assets/SEE/Layout/NodeLayouts/IncrementalTreeMap/Utils.cs
index cb5f703661..de22a2891b 100644
--- a/Assets/SEE/Layout/NodeLayouts/IncrementalTreeMap/Utils.cs
+++ b/Assets/SEE/Layout/NodeLayouts/IncrementalTreeMap/Utils.cs
@@ -13,7 +13,7 @@ internal static class Utils
///
/// Returns the item of the given collection that maximizes the given function.
///
- /// The collection whose maximum with respect to
+ /// The collection whose maximum with respect to
/// shall be returned
/// The function to be maximized
/// Item of that maximizes
@@ -25,7 +25,7 @@ public static T ArgMax(ICollection collection, Func eval)
/// Returns the item of the given collection that minimizes the given function.
///
- /// The collection whose minimum with respect to
+ /// The collection whose minimum with respect to
/// shall be returned
/// The function to be minimized
/// Item of that minimizes