package layered
import (
"fmt"
"github.com/d2lang/elk-go/internal/common/nodespacing"
"github.com/d2lang/elk-go/internal/core"
"github.com/d2lang/elk-go/internal/geometry"
basegraph "github.com/d2lang/elk-go/internal/graph"
)
type ImportMaps struct {
Nodes map [basegraph .NodeID ]LNodeID
Ports map [basegraph .PortID ]LPortID
Edges map [basegraph .EdgeID ]LEdgeID
Labels map [basegraph .LabelID ]LLabelID
Graphs map [basegraph .NodeID ]LGraphID
}
type ElkGraphImporter struct {
Model *basegraph .Model
Maps ImportMaps
}
func NewElkGraphImporter (model *basegraph .Model ) *ElkGraphImporter {
return &ElkGraphImporter {Model : model , Maps : ImportMaps {
Nodes : make (map [basegraph .NodeID ]LNodeID ), Ports : make (map [basegraph .PortID ]LPortID ),
Edges : make (map [basegraph .EdgeID ]LEdgeID ), Labels : make (map [basegraph .LabelID ]LLabelID ),
Graphs : make (map [basegraph .NodeID ]LGraphID ),
}}
}
func (importer *ElkGraphImporter ) ImportGraph (parent basegraph .NodeID ) (*LGraphArena , LGraphID , error ) {
if importer .Model == nil {
return nil , 0 , fmt .Errorf ("layered: cannot import a nil graph" )
}
arena := NewLGraphArena ()
topGraph := importer .createGraph (arena , parent )
importer .Maps .Graphs [parent ] = topGraph
if basegraph .Get (&arena .Graph (topGraph ).Properties , core .PartitioningActivate ) {
importer .addGraphProperty (arena , topGraph , GraphPropertyPartitions )
}
if err := importer .importExternalPorts (arena , parent , topGraph ); err != nil {
return nil , 0 , err
}
hierarchical := baseHierarchy (importer .Model .Node (parent )) == HierarchyIncludeChildren
if err := importer .importNodes (arena , parent , topGraph , hierarchical ); err != nil {
return nil , 0 , err
}
if err := importer .importEdges (arena , parent , topGraph , hierarchical ); err != nil {
return nil , 0 , err
}
if err := importer .importInsideSelfLoops (arena , parent , topGraph ); err != nil {
return nil , 0 , err
}
if baseValue , found := basegraph .TryGet (&arena .Graph (topGraph ).Properties , SpacingBaseValue ); found {
ApplySpacingBaseValue (&arena .Graph (topGraph ).Properties , baseValue )
}
return arena , topGraph , nil
}
func (importer *ElkGraphImporter ) importExternalPorts (
arena *LGraphArena ,
parent basegraph .NodeID ,
topGraph LGraphID ,
) error {
parentNode := importer .Model .Node (parent )
if len (parentNode .Ports ) == 0 {
return nil
}
for _ , portID := range parentNode .Ports {
importer .ensureDefinedExternalPortSide (arena , topGraph , portID )
}
hasExternalPorts , hasHyperedges := false , false
insideLabels := stringSetContains (
basegraph .Get (&parentNode .Properties , nodespacing .PortLabelsPlacement ), "INSIDE" )
for _ , portID := range parentNode .Ports {
edgeCount := importer .externalPortEdgeCount (parent , portID )
if edgeCount > 0 || insideLabels && len (importer .Model .Port (portID ).Labels ) > 0 {
hasExternalPorts = true
}
if edgeCount > 1 {
hasHyperedges = true
}
if hasExternalPorts && hasHyperedges {
break
}
}
if !hasExternalPorts {
return nil
}
importer .addGraphProperty (arena , topGraph , GraphPropertyExternalPorts )
if hasHyperedges {
importer .addGraphProperty (arena , topGraph , GraphPropertyHyperedges )
}
for _ , portID := range parentNode .Ports {
if err := importer .transformExternalPort (arena , parent , portID , topGraph ); err != nil {
return err
}
}
return nil
}
func (importer *ElkGraphImporter ) ensureDefinedExternalPortSide (
arena *LGraphArena ,
graphID LGraphID ,
portID basegraph .PortID ,
) {
port := importer .Model .Port (portID )
direction := basegraph .Get (&arena .Graph (graphID ).Properties , DirectionOption )
constraints := basegraph .Get (&arena .Graph (graphID ).Properties , PortConstraintsOption )
side := basegraph .Get (&port .Properties , PortSideOption )
if !constraints .IsSideFixed () {
side = DirectionPortSide (direction )
if importer .calculateExternalPortNetFlow (portID ) <= 0 {
side = side .Opposed ()
}
} else if side == UndefinedSide {
node := importer .Model .Node (port .Parent )
side = inferPortSide (
geometry .Point {X : node .Shape .Width , Y : node .Shape .Height },
port .Shape .Position (),
geometry .Point {X : port .Shape .Width , Y : port .Shape .Height },
direction ,
)
if side == UndefinedSide {
side = DirectionPortSide (direction )
}
}
basegraph .Set (&port .Properties , PortSideOption , side )
}
func (importer *ElkGraphImporter ) externalPortEdgeCount (
parent basegraph .NodeID ,
portID basegraph .PortID ,
) int {
port := importer .Model .Port (portID )
count := 0
seen := make (map [basegraph .EdgeID ]bool )
consider := func (edgeID basegraph .EdgeID ) {
if seen [edgeID ] {
return
}
seen [edgeID ] = true
edge := importer .Model .Edge (edgeID )
if importer .isInsideSelfLoop (parent , edgeID ) {
count ++
return
}
if len (edge .Sources ) == 0 || len (edge .Targets ) == 0 {
return
}
var opposite basegraph .NodeID
if shapeListContainsPort (edge .Sources , portID ) {
opposite = importer .Model .ShapeNode (edge .Targets [0 ])
} else {
opposite = importer .Model .ShapeNode (edge .Sources [0 ])
}
if oppositeNode := importer .Model .Node (opposite ); oppositeNode .Parent != nil && *oppositeNode .Parent == parent {
count ++
}
}
for _ , edgeID := range port .OutgoingEdges {
consider (edgeID )
}
for _ , edgeID := range port .IncomingEdges {
consider (edgeID )
}
return count
}
func (importer *ElkGraphImporter ) transformExternalPort (
arena *LGraphArena ,
parent basegraph .NodeID ,
portID basegraph .PortID ,
graphID LGraphID ,
) error {
original := importer .Model .Port (portID )
parentNode := importer .Model .Node (parent )
portSize := geometry .Point {X : original .Shape .Width , Y : original .Shape .Height }
portPosition := original .Shape .Position ().Add (portSize .Scale (.5 ))
constraints := basegraph .Get (&arena .Graph (graphID ).Properties , PortConstraintsOption )
if constraints == PortConstraintsUndefined {
constraints = PortConstraintsFree
}
side := basegraph .Get (&original .Properties , PortSideOption )
if side == UndefinedSide {
return fmt .Errorf ("layered: external port %s has no defined side" , original .Identifier )
}
if !basegraph .Has (&original .Properties , PortBorderOffset ) {
offset := 0.0
if original .Shape .X != 0 || original .Shape .Y != 0 {
offset = externalPortBorderOffset (parentNode , original , side )
}
basegraph .Set (&original .Properties , PortBorderOffset , offset )
}
parentSize := geometry .Point {X : parentNode .Shape .Width , Y : parentNode .Shape .Height }
dummyID := arena .CreateExternalPortDummy (
&original .Properties ,
constraints ,
side ,
importer .calculateExternalPortNetFlow (portID ),
&parentSize ,
&portPosition ,
portSize ,
basegraph .Get (&arena .Graph (graphID ).Properties , DirectionOption ),
graphID ,
)
basegraph .Set (&arena .Node (dummyID ).Properties , OriginalElement , PortOrigin (portID ))
basegraph .Set (&arena .Node (dummyID ).Properties , nodespacing .PortLabelsPlacement , []string {"OUTSIDE" })
dummyPortID := arena .Node (dummyID ).Ports [0 ]
arena .Port (dummyPortID ).ConnectedToExternalNodes = importer .isConnectedToExternalNodes (portID )
insideLabels := stringSetContains (
basegraph .Get (&parentNode .Properties , nodespacing .PortLabelsPlacement ), "INSIDE" )
fixedLabels := fixedPortLabelPlacement (&parentNode .Properties )
for _ , labelID := range original .Labels {
label := importer .Model .Label (labelID )
if label .Text == "" || basegraph .Get (&label .Properties , core .NoLayout ) {
continue
}
llabelID := importer .transformLabel (arena , labelID )
arena .Port (dummyPortID ).Labels = append (arena .Port (dummyPortID ).Labels , llabelID )
if !insideLabels {
insidePart := 0.0
if fixedLabels {
insidePart = portLabelInsidePart (label , original , side )
}
switch side {
case EastSide , WestSide :
arena .Label (llabelID ).Size .X = insidePart
case NorthSide , SouthSide :
arena .Label (llabelID ).Size .Y = insidePart
}
}
}
outerProperties := &basegraph .PropertyMap {}
if parentNode .Parent != nil {
outerProperties = &importer .Model .Node (*parentNode .Parent ).Properties
}
basegraph .Set (&arena .Node (dummyID ).Properties , SpacingLabelPortHorizontal ,
basegraph .Get (outerProperties , SpacingLabelPortHorizontal ))
basegraph .Set (&arena .Node (dummyID ).Properties , SpacingLabelPortVertical ,
basegraph .Get (outerProperties , SpacingLabelPortVertical ))
basegraph .Set (&arena .Node (dummyID ).Properties , SpacingLabelLabel ,
basegraph .Get (outerProperties , SpacingLabelLabel ))
arena .Graph (graphID ).LayerlessNodes = append (arena .Graph (graphID ).LayerlessNodes , dummyID )
importer .Maps .Ports [portID ] = dummyPortID
return nil
}
func (importer *ElkGraphImporter ) calculateExternalPortNetFlow (portID basegraph .PortID ) int {
port := importer .Model .Port (portID )
parent := port .Parent
outputVotes , inputVotes := 0 , 0
for _ , edgeID := range port .OutgoingEdges {
edge := importer .Model .Edge (edgeID )
if len (edge .Targets ) == 0 {
continue
}
selfLoop := core .IsSelfLoop (importer .Model , edgeID )
inside := importer .isInsideSelfLoop (parent , edgeID )
target := importer .Model .ShapeNode (edge .Targets [0 ])
switch {
case selfLoop && inside :
inputVotes ++
case selfLoop :
outputVotes ++
case importer .Model .Node (target ).Parent != nil && *importer .Model .Node (target ).Parent == parent || target == parent :
inputVotes ++
default :
outputVotes ++
}
}
for _ , edgeID := range port .IncomingEdges {
edge := importer .Model .Edge (edgeID )
if len (edge .Sources ) == 0 {
continue
}
selfLoop := core .IsSelfLoop (importer .Model , edgeID )
inside := importer .isInsideSelfLoop (parent , edgeID )
source := importer .Model .ShapeNode (edge .Sources [0 ])
switch {
case selfLoop && inside :
outputVotes ++
case selfLoop :
inputVotes ++
case importer .Model .Node (source ).Parent != nil && *importer .Model .Node (source ).Parent == parent || source == parent :
outputVotes ++
default :
inputVotes ++
}
}
return outputVotes - inputVotes
}
func (importer *ElkGraphImporter ) isConnectedToExternalNodes (portID basegraph .PortID ) bool {
port := importer .Model .Port (portID )
parent := port .Parent
for _ , edgeID := range port .OutgoingEdges {
edge := importer .Model .Edge (edgeID )
if len (edge .Targets ) > 0 && !importer .Model .IsDescendant (importer .Model .ShapeNode (edge .Targets [0 ]), parent ) {
return true
}
}
for _ , edgeID := range port .IncomingEdges {
edge := importer .Model .Edge (edgeID )
if len (edge .Sources ) > 0 && !importer .Model .IsDescendant (importer .Model .ShapeNode (edge .Sources [0 ]), parent ) {
return true
}
}
return false
}
func externalPortBorderOffset(node *basegraph .Node , port *basegraph .Port , side PortSide ) float64 {
switch side {
case NorthSide :
return -(port .Shape .Y + port .Shape .Height )
case EastSide :
return port .Shape .X - node .Shape .Width
case SouthSide :
return port .Shape .Y - node .Shape .Height
case WestSide :
return -(port .Shape .X + port .Shape .Width )
default :
return 0
}
}
func portLabelInsidePart(label *basegraph .Label , port *basegraph .Port , side PortSide ) float64 {
switch side {
case NorthSide :
return max (0 , label .Shape .Height +label .Shape .Y -port .Shape .Height )
case SouthSide :
return max (0 , -label .Shape .Y )
case EastSide :
return max (0 , -label .Shape .X )
case WestSide :
return max (0 , label .Shape .Width +label .Shape .X -port .Shape .Width )
default :
return 0
}
}
func shapeListContainsPort(shapes []basegraph .ShapeID , portID basegraph .PortID ) bool {
for _ , shape := range shapes {
if shape .Kind == basegraph .PortShape && shape .Port == portID {
return true
}
}
return false
}
func stringSetContains(values []string , value string ) bool {
for _ , candidate := range values {
if candidate == value {
return true
}
}
return false
}
func (importer *ElkGraphImporter ) createGraph (arena *LGraphArena , origin basegraph .NodeID ) LGraphID {
graphID := arena .AddGraph ()
graph := arena .Graph (graphID )
copyTypedProperties (&graph .Properties , &importer .Model .Node (origin ).Properties )
basegraph .Set (&graph .Properties , OriginalElement , NodeOrigin (origin ))
basegraph .Set (&graph .Properties , GraphProperties , GraphProperty (0 ))
graph .Padding = basegraph .Get (&graph .Properties , Padding )
if basegraph .Get (&graph .Properties , DirectionOption ) == DirectionUndefined {
basegraph .Set (&graph .Properties , DirectionOption , DirectionRight )
}
return graphID
}
func (importer *ElkGraphImporter ) importNodes (
arena *LGraphArena ,
parent basegraph .NodeID ,
topGraph LGraphID ,
hierarchical bool ,
) error {
if !hierarchical {
modelOrder := 0
for _ , nodeID := range importer .Model .Node (parent ).Children {
if basegraph .Get (&importer .Model .Node (nodeID ).Properties , core .NoLayout ) {
continue
}
lnode := importer .transformNode (arena , nodeID , topGraph )
if needsModelOrder (arena .Graph (topGraph )) &&
!basegraph .Get (&importer .Model .Node (nodeID ).Properties , NoModelOrder ) {
basegraph .Set (&arena .Node (lnode ).Properties , ModelOrder , modelOrder )
modelOrder ++
}
}
basegraph .Set (&arena .Graph (topGraph ).Properties , MaximumModelOrderNodes , modelOrder )
return nil
}
direction := basegraph .Get (&arena .Graph (topGraph ).Properties , DirectionOption )
queue := append ([]basegraph .NodeID (nil ), importer .Model .Node (parent ).Children ...)
modelOrder := 0
for len (queue ) > 0 {
nodeID := queue [0 ]
queue = queue [1 :]
node := importer .Model .Node (nodeID )
if basegraph .Get (&node .Properties , core .NoLayout ) {
continue
}
parentGraph := topGraph
if node .Parent != nil && *node .Parent != parent {
mappedParent , found := importer .Maps .Nodes [*node .Parent ]
if !found || arena .Node (mappedParent ).NestedGraph == nil {
return fmt .Errorf ("layered: parent of node %s has no nested graph" , node .Identifier )
}
parentGraph = *arena .Node (mappedParent ).NestedGraph
}
var nested *LGraphID
if len (node .Children ) > 0 && baseHierarchy (node ) == HierarchyIncludeChildren {
nestedID := importer .createGraph (arena , nodeID )
basegraph .Set (&arena .Graph (nestedID ).Properties , DirectionOption , direction )
importer .Maps .Graphs [nodeID ] = nestedID
nested = &nestedID
if baseValue , found := basegraph .TryGet (&arena .Graph (nestedID ).Properties , SpacingBaseValue ); found {
ApplySpacingBaseValue (&arena .Graph (nestedID ).Properties , baseValue )
}
}
lnode := importer .transformNode (arena , nodeID , parentGraph )
if needsModelOrder (arena .Graph (parentGraph )) &&
!basegraph .Get (&node .Properties , NoModelOrder ) {
basegraph .Set (&arena .Node (lnode ).Properties , ModelOrder , modelOrder )
modelOrder ++
}
if nested != nil {
arena .Node (lnode ).NestedGraph = nested
arena .Graph (*nested ).ParentNode = &lnode
queue = append (queue , node .Children ...)
}
}
basegraph .Set (&arena .Graph (topGraph ).Properties , MaximumModelOrderNodes , modelOrder )
return nil
}
func (importer *ElkGraphImporter ) transformNode (
arena *LGraphArena ,
nodeID basegraph .NodeID ,
graphID LGraphID ,
) LNodeID {
original := importer .Model .Node (nodeID )
lnodeID := arena .AddNode (graphID )
lnode := arena .Node (lnodeID )
copyTypedProperties (&lnode .Properties , &original .Properties )
copyIndividualSpacings (lnode , original )
basegraph .Set (&lnode .Properties , OriginalElement , NodeOrigin (nodeID ))
lnode .Position = original .Shape .Position ()
lnode .Size = geometry .Point {X : original .Shape .Width , Y : original .Shape .Height }
lnode .Margin = basegraph .Get (&original .Properties , nodespacing .Margins )
arena .Graph (graphID ).LayerlessNodes = append (arena .Graph (graphID ).LayerlessNodes , lnodeID )
importer .Maps .Nodes [nodeID ] = lnodeID
if len (original .Children ) > 0 || basegraph .Get (&original .Properties , core .InsideSelfLoopsActivate ) {
basegraph .Set (&lnode .Properties , CompoundNode , true )
}
constraints := basegraph .Get (&lnode .Properties , PortConstraintsOption )
if constraints == PortConstraintsUndefined {
constraints = PortConstraintsFree
basegraph .Set (&lnode .Properties , PortConstraintsOption , constraints )
} else if constraints != PortConstraintsFree {
importer .addGraphProperty (arena , graphID , GraphPropertyNonFreePorts )
}
direction := basegraph .Get (&arena .Graph (graphID ).Properties , DirectionOption )
assignPortModelOrder := needsModelOrder (arena .Graph (graphID )) &&
!basegraph .Get (&original .Properties , NoModelOrder )
for portModelOrder , portID := range original .Ports {
if basegraph .Get (&importer .Model .Port (portID ).Properties , core .NoLayout ) {
continue
}
lportID := importer .transformPort (arena , portID , lnodeID , graphID , direction , constraints )
if assignPortModelOrder {
basegraph .Set (&arena .Port (lportID ).Properties , ModelOrder , portModelOrder )
}
}
for _ , labelID := range original .Labels {
label := importer .Model .Label (labelID )
if label .Text == "" || basegraph .Get (&label .Properties , core .NoLayout ) {
continue
}
lnode .Labels = append (lnode .Labels , importer .transformLabel (arena , labelID ))
}
if basegraph .Get (&lnode .Properties , core .CommentBox ) {
importer .addGraphProperty (arena , graphID , GraphPropertyComments )
}
if basegraph .Get (&lnode .Properties , core .Hypernode ) {
importer .addGraphProperty (arena , graphID , GraphPropertyHypernodes )
importer .addGraphProperty (arena , graphID , GraphPropertyHyperedges )
basegraph .Set (&lnode .Properties , PortConstraintsOption , PortConstraintsFree )
}
return lnodeID
}
func copyIndividualSpacings(target *LNode , source *basegraph .Node ) {
var individual IndividualSpacings
if raw , found := source .Properties .Raw (core .SpacingIndividual .ID ); found {
switch typed := raw .(type ) {
case *core .IndividualSpacings :
if typed != nil {
individual .Properties .CopyFrom (&typed .Properties )
}
case core .IndividualSpacings :
individual .Properties .CopyFrom (&typed .Properties )
case IndividualSpacings :
individual .Properties .CopyFrom (&typed .Properties )
}
}
if !individual .Properties .Empty () {
basegraph .Set (&target .Properties , SpacingIndividual , individual )
}
}
func (importer *ElkGraphImporter ) transformPort (
arena *LGraphArena ,
portID basegraph .PortID ,
nodeID LNodeID ,
graphID LGraphID ,
direction Direction ,
constraints PortConstraints ,
) LPortID {
original := importer .Model .Port (portID )
lportID := arena .AddPort ()
lport := arena .Port (lportID )
copyTypedProperties (&lport .Properties , &original .Properties )
basegraph .Set (&lport .Properties , OriginalElement , PortOrigin (portID ))
lport .Position = original .Shape .Position ()
lport .Size = geometry .Point {X : original .Shape .Width , Y : original .Shape .Height }
arena .SetPortNode (lportID , &nodeID )
side := basegraph .Get (&lport .Properties , PortSideOption )
if side == UndefinedSide {
if constraints .IsSideFixed () {
side = inferPortSide (arena .Node (nodeID ).Size , lport .Position , lport .Size , direction )
} else if len (original .IncomingEdges )-len (original .OutgoingEdges ) > 0 {
side = DirectionPortSide (direction )
} else {
side = DirectionPortSide (direction ).Opposed ()
}
}
arena .SetPortSide (lportID , side )
for _ , labelID := range original .Labels {
label := importer .Model .Label (labelID )
if label .Text == "" || basegraph .Get (&label .Properties , core .NoLayout ) {
continue
}
lport .Labels = append (lport .Labels , importer .transformLabel (arena , labelID ))
}
if (direction == DirectionRight || direction == DirectionLeft ) && (side == NorthSide || side == SouthSide ) ||
(direction == DirectionUp || direction == DirectionDown ) && (side == EastSide || side == WestSide ) {
importer .addGraphProperty (arena , graphID , GraphPropertyNorthSouthPorts )
}
importer .Maps .Ports [portID ] = lportID
return lportID
}
func (importer *ElkGraphImporter ) transformLabel (arena *LGraphArena , labelID basegraph .LabelID ) LLabelID {
original := importer .Model .Label (labelID )
llabelID := arena .AddLabel (original .Text )
llabel := arena .Label (llabelID )
copyTypedProperties (&llabel .Properties , &original .Properties )
basegraph .Set (&llabel .Properties , OriginalElement , LabelOrigin (labelID ))
llabel .Position = original .Shape .Position ()
llabel .Size = geometry .Point {X : original .Shape .Width , Y : original .Shape .Height }
importer .Maps .Labels [labelID ] = llabelID
return llabelID
}
func (importer *ElkGraphImporter ) importEdges (
arena *LGraphArena ,
parent basegraph .NodeID ,
topGraph LGraphID ,
hierarchical bool ,
) error {
containers := []basegraph .NodeID {parent }
if hierarchical {
for index := 0 ; index < len (containers ); index ++ {
for _ , child := range importer .Model .Node (containers [index ]).Children {
if _ , partOfRun := importer .Maps .Graphs [child ]; partOfRun {
containers = append (containers , child )
}
}
}
}
modelOrder := 0
for _ , container := range containers {
for _ , edgeID := range append ([]basegraph .EdgeID (nil ), importer .Model .Node (container ).ContainedEdges ...) {
edge := importer .Model .Edge (edgeID )
if needsModelOrder (arena .Graph (topGraph )) {
basegraph .Set (&edge .Properties , ModelOrder , modelOrder )
modelOrder ++
}
if len (edge .Sources ) != 1 || len (edge .Targets ) != 1 {
return fmt .Errorf ("layered: edge %s is a hyperedge; hyperedges are unsupported" , edge .Identifier )
}
if basegraph .Get (&edge .Properties , core .NoLayout ) {
continue
}
if importer .isInsideSelfLoop (importer .Model .ShapeNode (edge .Sources [0 ]), edgeID ) {
continue
}
sourceNode := importer .Model .ShapeNode (edge .Sources [0 ])
targetNode := importer .Model .ShapeNode (edge .Targets [0 ])
if _ , sourceFound := importer .mappedEndpointNode (arena , edge .Sources [0 ]); !sourceFound {
continue
}
if _ , targetFound := importer .mappedEndpointNode (arena , edge .Targets [0 ]); !targetFound {
continue
}
graphID := importer .edgeGraph (arena , topGraph , parent , container , sourceNode , targetNode )
ledgeID , err := importer .transformEdge (arena , edgeID , graphID )
if err != nil {
return err
}
if needsModelOrder (arena .Graph (topGraph )) {
basegraph .Set (&arena .Edge (ledgeID ).Properties , ModelOrder ,
basegraph .Get (&edge .Properties , ModelOrder ))
}
if sourceNode != targetNode && importer .Model .Node (sourceNode ).Parent != importer .Model .Node (targetNode ).Parent {
if origin , found := importer .Maps .Graphs [container ]; found {
basegraph .Set (&arena .Edge (ledgeID ).Properties , CoordinateSystemOrigin , origin )
}
}
}
}
return nil
}
func (importer *ElkGraphImporter ) importInsideSelfLoops (
arena *LGraphArena ,
parent basegraph .NodeID ,
topGraph LGraphID ,
) error {
parentNode := importer .Model .Node (parent )
if parentNode .Parent == nil {
return nil
}
for _ , edgeID := range importer .Model .Node (*parentNode .Parent ).ContainedEdges {
if !importer .isInsideSelfLoop (parent , edgeID ) {
continue
}
if _ , alreadyImported := importer .Maps .Edges [edgeID ]; alreadyImported {
continue
}
if _ , err := importer .transformEdge (arena , edgeID , topGraph ); err != nil {
return err
}
}
return nil
}
func (importer *ElkGraphImporter ) isInsideSelfLoop (
nodeID basegraph .NodeID ,
edgeID basegraph .EdgeID ,
) bool {
edge := importer .Model .Edge (edgeID )
return basegraph .Get (&importer .Model .Node (nodeID ).Properties , core .InsideSelfLoopsActivate ) &&
basegraph .Get (&edge .Properties , core .InsideSelfLoopsEdge ) &&
core .IsSelfLoop (importer .Model , edgeID ) &&
len (edge .Sources ) > 0 && importer .Model .ShapeNode (edge .Sources [0 ]) == nodeID
}
func (importer *ElkGraphImporter ) edgeGraph (
arena *LGraphArena ,
topGraph LGraphID ,
root , containing , source , target basegraph .NodeID ,
) LGraphID {
if importer .Model .IsDescendant (target , source ) {
if graphID , found := importer .Maps .Graphs [source ]; found {
return graphID
}
}
if importer .Model .IsDescendant (source , target ) {
if graphID , found := importer .Maps .Graphs [target ]; found {
return graphID
}
}
if containing == root {
return topGraph
}
if graphID , found := importer .Maps .Graphs [containing ]; found {
return graphID
}
if lnode , found := importer .Maps .Nodes [containing ]; found {
return arena .NodeGraph (lnode )
}
return topGraph
}
func (importer *ElkGraphImporter ) transformEdge (
arena *LGraphArena ,
edgeID basegraph .EdgeID ,
graphID LGraphID ,
) (LEdgeID , error ) {
original := importer .Model .Edge (edgeID )
if len (original .Sources ) != 1 || len (original .Targets ) != 1 {
return 0 , fmt .Errorf ("layered: edge %s is a hyperedge; hyperedges are unsupported" , original .Identifier )
}
sourceNodeID := importer .Model .ShapeNode (original .Sources [0 ])
targetNodeID := importer .Model .ShapeNode (original .Targets [0 ])
sourceNode , sourceFound := importer .mappedEndpointNode (arena , original .Sources [0 ])
targetNode , targetFound := importer .mappedEndpointNode (arena , original .Targets [0 ])
if !sourceFound || !targetFound {
return 0 , fmt .Errorf ("layered: source or target of edge %s could not be transformed" , original .Identifier )
}
sourcePort , found := importer .mappedPort (original .Sources [0 ])
if !found {
sourcePort = importer .createDefaultPort (arena , graphID , sourceNode , PortTypeOutput )
}
targetPort , found := importer .mappedPort (original .Targets [0 ])
if !found {
targetPort = importer .createDefaultPort (arena , arena .NodeGraph (targetNode ), targetNode , PortTypeInput )
}
ledgeID := arena .AddEdge ()
ledge := arena .Edge (ledgeID )
copyTypedProperties (&ledge .Properties , &original .Properties )
basegraph .Set (&ledge .Properties , OriginalElement , EdgeOrigin (edgeID ))
basegraph .Unset (&ledge .Properties , JunctionPoints )
arena .SetEdgeSource (ledgeID , &sourcePort )
arena .SetEdgeTarget (ledgeID , &targetPort )
importer .Maps .Edges [edgeID ] = ledgeID
if sourceNodeID == targetNodeID {
importer .addGraphProperty (arena , graphID , GraphPropertySelfLoops )
}
if len (arena .Port (sourcePort ).IncomingEdges ) > 1 || len (arena .Port (sourcePort ).OutgoingEdges ) > 1 ||
len (arena .Port (targetPort ).IncomingEdges ) > 1 || len (arena .Port (targetPort ).OutgoingEdges ) > 1 {
importer .addGraphProperty (arena , graphID , GraphPropertyHyperedges )
}
for _ , labelID := range original .Labels {
label := importer .Model .Label (labelID )
if label .Text == "" || basegraph .Get (&label .Properties , core .NoLayout ) {
continue
}
llabelID := importer .transformLabel (arena , labelID )
ledge .Labels = append (ledge .Labels , llabelID )
switch basegraph .Get (&arena .Label (llabelID ).Properties , EdgeLabelsPlacement ) {
case EdgeLabelPlacementHead , EdgeLabelPlacementTail :
importer .addGraphProperty (arena , graphID , GraphPropertyEndLabels )
case EdgeLabelPlacementCenter :
importer .addGraphProperty (arena , graphID , GraphPropertyCenterLabels )
}
}
if (basegraph .Get (&arena .Graph (graphID ).Properties , CrossingMinimization ) == CrossingMinimizationInteractive ||
basegraph .Get (&arena .Graph (graphID ).Properties , NodePlacement ) == NodePlacementInteractive ) &&
len (original .Sections ) > 0 {
section := importer .Model .Section (original .Sections [0 ])
if len (section .BendPoints ) > 0 {
basegraph .Set (&ledge .Properties , OriginalBendPoints , section .BendPoints .Clone ())
}
}
return ledgeID , nil
}
func (importer *ElkGraphImporter ) mappedEndpointNode (
arena *LGraphArena ,
shape basegraph .ShapeID ,
) (LNodeID , bool ) {
if shape .Kind == basegraph .PortShape {
if portID , found := importer .Maps .Ports [shape .Port ]; found && arena .Port (portID ).Node != nil {
return *arena .Port (portID ).Node , true
}
}
nodeID , found := importer .Maps .Nodes [importer .Model .ShapeNode (shape )]
return nodeID , found
}
func (importer *ElkGraphImporter ) mappedPort (shape basegraph .ShapeID ) (LPortID , bool ) {
if shape .Kind != basegraph .PortShape {
return 0 , false
}
port , found := importer .Maps .Ports [shape .Port ]
return port , found
}
func (importer *ElkGraphImporter ) createDefaultPort (
arena *LGraphArena ,
graphID LGraphID ,
nodeID LNodeID ,
portType PortType ,
) LPortID {
portID := arena .AddPort ()
arena .SetPortNode (portID , &nodeID )
direction := basegraph .Get (&arena .Graph (graphID ).Properties , DirectionOption )
side := DirectionPortSide (direction )
if portType == PortTypeInput {
side = side .Opposed ()
}
arena .SetPortSide (portID , side )
return portID
}
func (importer *ElkGraphImporter ) addGraphProperty (arena *LGraphArena , graphID LGraphID , property GraphProperty ) {
properties := basegraph .Get (&arena .Graph (graphID ).Properties , GraphProperties )
basegraph .Set (&arena .Graph (graphID ).Properties , GraphProperties , properties |property )
}
func needsModelOrder(graph *LGraph ) bool {
cycle := basegraph .Get (&graph .Properties , CycleBreaking )
layering := basegraph .Get (&graph .Properties , Layering )
promotion := basegraph .Get (&graph .Properties , NodePromotionStrategyOption )
return basegraph .Get (&graph .Properties , ConsiderModelOrder ) != OrderingNone ||
cycle == CycleBreakingModelOrder || cycle == CycleBreakingGreedyModelOrder ||
cycle == CycleBreakingSCCConnectivity || cycle == CycleBreakingSCCNodeType ||
cycle == CycleBreakingDFSNodeOrder || cycle == CycleBreakingBFSNodeOrder ||
layering == LayeringBreadthFirstModelOrder || layering == LayeringDepthFirstModelOrder ||
promotion == NodePromotionModelOrderLeftToRight ||
promotion == NodePromotionModelOrderRightToLeft ||
basegraph .Get (&graph .Properties , ForceNodeModelOrder ) ||
basegraph .Get (&graph .Properties , ComponentOrdering ) != ComponentOrderingNone ||
basegraph .Get (&graph .Properties , CrossingCounterNodeInfluence ) != 0 ||
basegraph .Get (&graph .Properties , CrossingCounterPortInfluence ) != 0
}
func baseHierarchy(node *basegraph .Node ) HierarchyHandling {
value , _ := node .Properties .Raw (core .HierarchyHandling .ID )
if hierarchy , found := parseHierarchyHandling (fmt .Sprint (value )); found {
return hierarchy
}
return HierarchySeparateChildren
}
func inferPortSide(nodeSize , position , size geometry .Point , direction Direction ) PortSide {
center := position .Add (size .Scale (0.5 ))
distances := []struct {
side PortSide
value float64
}{
{NorthSide , center .Y },
{EastSide , nodeSize .X - center .X },
{SouthSide , nodeSize .Y - center .Y },
{WestSide , center .X },
}
best := distances [0 ]
for _ , candidate := range distances [1 :] {
if candidate .value < best .value {
best = candidate
}
}
if best .value < 0 {
return DirectionPortSide (direction )
}
return best .side
}
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 .