package hierarchy
import (
"math"
"slices"
"github.com/pancsta/d2/lib/geo"
"github.com/pancsta/d2/d2layouts/d2talalayout/internal/layoutgraph"
)
const crossingComparisonPrecision = 0.0001
type crossingScratch struct {
segments []layoutgraph .CrossingSegment
}
func initializeRanks(byLevel map [int ][]*placementNode ) {
for i := 0 ; i < len (byLevel ); i ++ {
computeLevelRanks (byLevel [i ])
}
}
func minimizeHierarchyCrossings(byLevel map [int ][]*placementNode ) {
widestLevel := 0
for _ , nodes := range byLevel {
widestLevel = max (widestLevel , len (nodes ))
}
var scratch crossingScratch
iterCount := widestLevel
for n := 0 ; n < iterCount ; n ++ {
for level := 0 ; level < len (byLevel ); level ++ {
minimizeCrossings (byLevel [level ], byLevel , level > 0 , &scratch )
}
}
}
func minimizeCrossings(nodes []*placementNode , byLevel map [int ][]*placementNode , isTopDown bool , scratch *crossingScratch ) {
sortLevelNodesByAdacencyPosition (nodes , isTopDown )
computeLevelRanks (byLevel [nodes [0 ].level ])
segments := scratch .crossLevelSegments (nodes , true , true )
crossings := countCrossings (segments )
length := addLengths (segments )
for j := 0 ; j < len (nodes ); j ++ {
newCrossings , newLength , swapIndex := bestIndexBySwappingNeighbors (nodes , j , byLevel , scratch )
if newCrossings < crossings {
crossings = newCrossings
length = newLength
nodes [j ], nodes [swapIndex ] = nodes [swapIndex ], nodes [j ]
} else if newCrossings == crossings && geo .PrecisionCompare (newLength , length , crossingComparisonPrecision ) == -1 {
crossings = newCrossings
length = newLength
nodes [j ], nodes [swapIndex ] = nodes [swapIndex ], nodes [j ]
}
computeLevelRanks (byLevel [nodes [0 ].level ])
}
for _ , node := range nodes {
if len (node .children ) > 0 && node .optimizeChildrenCrossings {
minimizeCrossings (node .children , byLevel , isTopDown , scratch )
}
}
}
func bestIndexBySwappingNeighbors(levelNodes []*placementNode , nodeIndex int , byLevel map [int ][]*placementNode , scratch *crossingScratch ) (int64 , float64 , int ) {
bestCrossing := int64 (math .MaxInt64 )
bestLength := math .Inf (1 )
bestIndex := 0
for i := nodeIndex + 1 ; i < nodeIndex +4 ; i ++ {
j := i % len (levelNodes )
levelNodes [nodeIndex ], levelNodes [j ] = levelNodes [j ], levelNodes [nodeIndex ]
computeLevelRanks (byLevel [levelNodes [nodeIndex ].level ])
segments := scratch .crossLevelSegments (levelNodes , true , true )
newCrossings := countCrossings (segments )
newLength := addLengths (segments )
if newCrossings < bestCrossing {
bestCrossing = newCrossings
bestIndex = j
bestLength = newLength
} else if newCrossings == bestCrossing && geo .PrecisionCompare (newLength , bestLength , crossingComparisonPrecision ) == -1 {
bestCrossing = newCrossings
bestIndex = j
bestLength = newLength
}
levelNodes [j ], levelNodes [nodeIndex ] = levelNodes [nodeIndex ], levelNodes [j ]
}
return bestCrossing , bestLength , bestIndex
}
func (scratch *crossingScratch ) crossLevelSegments (nodes []*placementNode , aboves , belows bool ) []layoutgraph .CrossingSegment {
segmentCount := 0
iterAllDescendants (nodes , func (pn *placementNode ) {
if aboves {
segmentCount += len (pn .aboves )
}
if belows {
segmentCount += len (pn .belows )
}
})
if cap (scratch .segments ) < segmentCount {
scratch .segments = make ([]layoutgraph .CrossingSegment , 0 , segmentCount )
} else {
scratch .segments = scratch .segments [:0 ]
}
iterAllDescendants (nodes , func (pn *placementNode ) {
if aboves {
for connected := range pn .aboves {
scratch .segments = append (scratch .segments , layoutgraph .CrossingSegment {
Start : geo .Point {X : float64 (pn .rank ), Y : float64 (pn .level )},
End : geo .Point {X : float64 (connected .rank ), Y : float64 (connected .level )},
})
}
}
if belows {
for connected := range pn .belows {
scratch .segments = append (scratch .segments , layoutgraph .CrossingSegment {
Start : geo .Point {X : float64 (pn .rank ), Y : float64 (pn .level )},
End : geo .Point {X : float64 (connected .rank ), Y : float64 (connected .level )},
})
}
}
})
slices .SortStableFunc (scratch .segments , func (a , b layoutgraph .CrossingSegment ) int {
if a .Start .X == b .Start .X {
switch {
case a .End .X < b .End .X :
return -1
case b .End .X < a .End .X :
return 1
default :
return 0
}
}
switch {
case a .Start .X < b .Start .X :
return -1
case b .Start .X < a .Start .X :
return 1
default :
return 0
}
})
return scratch .segments
}
func iterAllDescendants(nodes []*placementNode , f func (*placementNode )) {
for _ , pn := range nodes {
f (pn )
}
for _ , pn := range nodes {
iterAllDescendants (pn .children , f )
}
}
func allDescendants(nodes []*placementNode , forOptimization bool ) []*placementNode {
allNodes := make ([]*placementNode , len (nodes ))
copy (allNodes , nodes )
for _ , pn := range nodes {
if !forOptimization || pn .optimizeChildrenCrossings {
allNodes = append (allNodes , allDescendants (pn .children , forOptimization )...)
}
}
return allNodes
}
func addLengths(segments []layoutgraph .CrossingSegment ) float64 {
length := 0.0
for _ , s := range segments {
length += geo .EuclideanDistance (s .Start .X , s .Start .Y , s .End .X , s .End .Y )
}
return length
}
func countCrossings(segments []layoutgraph .CrossingSegment ) int64 {
return layoutgraph .CountSegmentCrossings (segments )
}
func sortLevelNodesByAdacencyPosition(nodes []*placementNode , useConnectionsAbove bool ) {
adjacentLevelAverage := func (pn *placementNode ) float64 {
nodes := pn .aboves
if !useConnectionsAbove {
nodes = pn .belows
}
if len (nodes ) == 0 {
return 0
}
sum := 0
for connected := range nodes {
sum += connected .rank
}
return float64 (sum ) / float64 (len (nodes ))
}
slices .SortStableFunc (nodes , func (a , b *placementNode ) int {
aAverage := adjacentLevelAverage (a )
bAverage := adjacentLevelAverage (b )
switch {
case aAverage < bAverage :
return -1
case bAverage < aAverage :
return 1
default :
return 0
}
})
}
func computeLevelRanks(nodes []*placementNode ) {
stack := make ([]*placementNode , len (nodes ))
for i := 0 ; i < len (nodes ); i ++ {
stack [i ] = nodes [len (nodes )-i -1 ]
}
for rank := 0 ; len (stack ) > 0 ; rank ++ {
current := stack [len (stack )-1 ]
stack = stack [:len (stack )-1 ]
current .rank = rank
for i := len (current .children ) - 1 ; i > -1 ; i -- {
stack = append (stack , current .children [i ])
}
}
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .