package core
import (
"fmt"
"github.com/d2lang/elk-go/internal/geometry"
"github.com/d2lang/elk-go/internal/graph"
)
const DefaultAlgorithmID = "org.eclipse.elk.layered"
type Engine struct {
Options *OptionRegistry
Algorithms *AlgorithmRegistry
Monitor ProgressMonitor
}
func NewEngine (options *OptionRegistry , algorithms *AlgorithmRegistry ) *Engine {
if options == nil {
options = NewOptionRegistry ()
}
if algorithms == nil {
algorithms = NewAlgorithmRegistry ()
}
return &Engine {Options : options , Algorithms : algorithms , Monitor : NullProgressMonitor }
}
func (engine *Engine ) Layout (model *graph .Model ) error {
if model == nil || len (model .Nodes ) == 0 {
return fmt .Errorf ("layout graph must contain a root node" )
}
if engine .Monitor == nil {
engine .Monitor = NullProgressMonitor
}
if err := engine .resolveAlgorithms (model ); err != nil {
return err
}
_ , err := engine .layoutRecursively (model , model .Root , engine .Monitor )
return err
}
func (engine *Engine ) resolveAlgorithms (model *graph .Model ) error {
nodes := append ([]graph .NodeID {model .Root }, model .Descendants (model .Root )...)
for _ , nodeID := range nodes {
node := model .Node (nodeID )
if graph .Get (&node .Properties , NoLayout ) {
continue
}
hasLayoutContent := len (node .Children ) > 0 || len (gatherInsideSelfLoops (model , nodeID )) > 0
algorithmID := graph .Get (&node .Properties , Algorithm )
algorithm := engine .Algorithms .BySuffix (algorithmID )
if algorithm == nil && algorithmID == "" && hasLayoutContent {
algorithm = engine .Algorithms .BySuffix (DefaultAlgorithmID )
}
if algorithm == nil {
if !hasLayoutContent {
continue
}
if algorithmID == "" {
return fmt .Errorf ("default layout algorithm %q is not registered" , DefaultAlgorithmID )
}
return fmt .Errorf ("layout algorithm %q not found for %s" , algorithmID , nodePath (model , nodeID ))
}
graph .Set (&node .Properties , ResolvedAlgorithm , algorithm )
}
return nil
}
func (engine *Engine ) layoutRecursively (model *graph .Model , parent graph .NodeID , monitor ProgressMonitor ) ([]graph .EdgeID , error ) {
node := model .Node (parent )
insideSelfLoops := gatherInsideSelfLoops (model , parent )
if graph .Get (&node .Properties , NoLayout ) || len (node .Children ) == 0 && len (insideSelfLoops ) == 0 {
return nil , nil
}
algorithm , found := graph .TryGet (&node .Properties , ResolvedAlgorithm )
if !found || algorithm == nil {
return nil , fmt .Errorf ("resolved algorithm is not set for %s" , nodePath (model , parent ))
}
engine .evaluateHierarchyHandling (model , parent )
supportsInsideSelfLoops := algorithm .Features .Has (FeatureInsideSelfLoops )
if len (node .Children ) == 0 && len (insideSelfLoops ) > 0 && !supportsInsideSelfLoops {
return nil , nil
}
childrenInsideSelfLoops := make ([]graph .EdgeID , 0 )
includeChildren := graph .Get (&node .Properties , HierarchyHandling ) == "INCLUDE_CHILDREN" &&
(algorithm .Features .Has (FeatureCompound ) || algorithm .Features .Has (FeatureClusters ))
topdown := graph .Get (&node .Properties , TopdownLayout )
algorithmExecuted := false
if includeChildren {
if topdown {
return nil , fmt .Errorf ("Topdown layout cannot be used together with hierarchy handling." )
}
queue := append ([]graph .NodeID (nil ), node .Children ...)
for len (queue ) > 0 {
childID := queue [0 ]
queue = queue [1 :]
engine .evaluateHierarchyHandling (model , childID )
child := model .Node (childID )
childAlgorithm , hasChildAlgorithm := graph .TryGet (&child .Properties , ResolvedAlgorithm )
stop := graph .Get (&child .Properties , HierarchyHandling ) == "SEPARATE_CHILDREN"
switches := graph .Has (&child .Properties , Algorithm ) && (!hasChildAlgorithm || childAlgorithm .ID != algorithm .ID )
if stop || switches {
childLoops , err := engine .layoutRecursively (
model , childID , monitor .Subtask (float64 (len (child .Children ))))
if err != nil {
return nil , err
}
childrenInsideSelfLoops = append (childrenInsideSelfLoops , childLoops ...)
graph .Set (&child .Properties , HierarchyHandling , "SEPARATE_CHILDREN" )
applyScale (model , childID )
} else {
queue = append (queue , child .Children ...)
}
}
} else {
if topdown {
if err := engine .layoutTopdown (model , parent , algorithm , monitor ); err != nil {
return nil , err
}
algorithmExecuted = true
}
children := append ([]graph .NodeID (nil ), node .Children ...)
for _ , childID := range children {
childLoops , err := engine .layoutRecursively (
model , childID , monitor .Subtask (float64 (len (model .Node (childID ).Children ))))
if err != nil {
return nil , err
}
childrenInsideSelfLoops = append (childrenInsideSelfLoops , childLoops ...)
applyScale (model , childID )
}
}
if monitor .Canceled () {
return nil , nil
}
for _ , edgeID := range childrenInsideSelfLoops {
graph .Set (&model .Edge (edgeID ).Properties , NoLayout , true )
}
if !algorithmExecuted {
if err := engine .executeAlgorithm (model , parent , algorithm , monitor ); err != nil {
return nil , err
}
}
for _ , edgeID := range childrenInsideSelfLoops {
postProcessInsideSelfLoop (model , edgeID )
}
if supportsInsideSelfLoops {
return insideSelfLoops , nil
}
return nil , nil
}
func (engine *Engine ) executeAlgorithm (
model *graph .Model ,
parent graph .NodeID ,
algorithm *AlgorithmData ,
monitor ProgressMonitor ,
) error {
provider := algorithm .Create ()
if provider == nil {
return fmt .Errorf ("layout algorithm %q returned a nil provider" , algorithm .ID )
}
monitor .Begin (algorithm .ID , float64 (len (model .Node (parent ).Children )))
err := provider .Layout (model , parent , monitor )
monitor .Done ()
if err != nil {
return fmt .Errorf ("layout algorithm %s: %w" , algorithm .ID , err )
}
return nil
}
func gatherInsideSelfLoops(model *graph .Model , nodeID graph .NodeID ) []graph .EdgeID {
node := model .Node (nodeID )
if !graph .Get (&node .Properties , InsideSelfLoopsActivate ) {
return nil
}
result := make ([]graph .EdgeID , 0 )
seen := make (map [graph .EdgeID ]bool )
consider := func (edgeID graph .EdgeID ) {
if seen [edgeID ] {
return
}
seen [edgeID ] = true
if graph .Get (&model .Edge (edgeID ).Properties , InsideSelfLoopsEdge ) &&
IsSelfLoop (model , edgeID ) && insideSelfLoopNode (model , edgeID ) == nodeID {
result = append (result , edgeID )
}
}
for _ , edgeID := range node .OutgoingEdges {
consider (edgeID )
}
for _ , portID := range node .Ports {
for _ , edgeID := range model .Port (portID ).OutgoingEdges {
consider (edgeID )
}
}
return result
}
func insideSelfLoopNode(model *graph .Model , edgeID graph .EdgeID ) graph .NodeID {
edge := model .Edge (edgeID )
if len (edge .Sources ) > 0 {
return model .ShapeNode (edge .Sources [0 ])
}
return 0
}
func postProcessInsideSelfLoop(model *graph .Model , edgeID graph .EdgeID ) {
edge := model .Edge (edgeID )
if len (edge .Sources ) == 0 || len (edge .Sections ) == 0 {
return
}
offset := model .Node (model .ShapeNode (edge .Sources [0 ])).Shape .Position ()
section := model .Section (edge .Sections [0 ])
section .Start = section .Start .Add (offset )
section .End = section .End .Add (offset )
section .BendPoints .Offset (offset )
if junctions , found := graph .TryGet (&edge .Properties , JunctionPoints ); found {
junctions .Offset (offset )
graph .Set (&edge .Properties , JunctionPoints , junctions )
}
}
func (engine *Engine ) evaluateHierarchyHandling (model *graph .Model , nodeID graph .NodeID ) {
node := model .Node (nodeID )
if graph .Get (&node .Properties , HierarchyHandling ) != "INHERIT" {
return
}
value := "SEPARATE_CHILDREN"
if node .Parent != nil {
value = graph .Get (&model .Node (*node .Parent ).Properties , HierarchyHandling )
}
graph .Set (&node .Properties , HierarchyHandling , value )
}
func applyScale(model *graph .Model , nodeID graph .NodeID ) {
scale := graph .Get (&model .Node (nodeID ).Properties , ScaleFactor )
if scale == 1 {
return
}
node := model .Node (nodeID )
node .Shape .Width *= scale
node .Shape .Height *= scale
scaleShape := func (shape *graph .Shape , properties *graph .PropertyMap ) {
shape .X *= scale
shape .Y *= scale
shape .Width *= scale
shape .Height *= scale
if anchor , found := graph .TryGet (properties , PortAnchor ); found {
anchor .X *= scale
anchor .Y *= scale
graph .Set (properties , PortAnchor , anchor )
}
}
for _ , labelID := range node .Labels {
label := model .Label (labelID )
scaleShape (&label .Shape , &label .Properties )
}
for _ , portID := range node .Ports {
port := model .Port (portID )
scaleShape (&port .Shape , &port .Properties )
for _ , labelID := range port .Labels {
label := model .Label (labelID )
scaleShape (&label .Shape , &label .Properties )
}
}
}
func TranslateSection (model *graph .Model , sectionID graph .SectionID , offset geometry .Point ) {
section := model .Section (sectionID )
section .Start = section .Start .Add (offset )
section .End = section .End .Add (offset )
section .BendPoints .Offset (offset )
}
func IsSelfLoop (model *graph .Model , edgeID graph .EdgeID ) bool {
edge := model .Edge (edgeID )
if len (edge .Sources )+len (edge .Targets ) == 0 {
return false
}
first := graph .NodeID (0 )
initialized := false
for _ , shape := range append (append ([]graph .ShapeID (nil ), edge .Sources ...), edge .Targets ...) {
node := model .ShapeNode (shape )
if !initialized {
first , initialized = node , true
} else if node != first {
return false
}
}
return true
}
func nodePath(model *graph .Model , nodeID graph .NodeID ) string {
parts := make ([]string , 0 )
for current := &nodeID ; current != nil ; current = model .Node (*current ).Parent {
node := model .Node (*current )
value := node .Identifier
if !node .IdentifierPresent {
value = fmt .Sprintf ("#%d" , *current )
}
parts = append (parts , value )
}
for left , right := 0 , len (parts )-1 ; left < right ; left , right = left +1 , right -1 {
parts [left ], parts [right ] = parts [right ], parts [left ]
}
result := ""
for index , part := range parts {
if index > 0 {
result += " > "
}
result += part
}
return result
}
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 .