package layered
import (
"fmt"
"strings"
"github.com/d2lang/elk-go/internal/basicoptions"
"github.com/d2lang/elk-go/internal/common/elkutil"
"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 ElkGraphLayoutTransferrer struct {
Model *basegraph .Model
Maps ImportMaps
}
func NewElkGraphLayoutTransferrer (model *basegraph .Model , maps ImportMaps ) *ElkGraphLayoutTransferrer {
return &ElkGraphLayoutTransferrer {Model : model , Maps : maps }
}
func (transferrer *ElkGraphLayoutTransferrer ) ApplyLayout (
arena *LGraphArena ,
topGraph LGraphID ,
) error {
if transferrer .Model == nil {
return fmt .Errorf ("layered: cannot transfer into a nil graph" )
}
if err := transferrer .applyGraph (arena , topGraph , make (map [LGraphID ]bool )); err != nil {
return err
}
seenEdges := make (map [basegraph .EdgeID ]bool )
for index := range arena .Edges {
edgeID := LEdgeID (index )
origin , found := basegraph .TryGet (&arena .Edge (edgeID ).Properties , OriginalElement )
if !found || origin .Kind != OriginEdge || seenEdges [origin .Edge ] {
continue
}
seenEdges [origin .Edge ] = true
transferrer .applyEdge (arena , edgeID , origin .Edge )
}
return nil
}
func (transferrer *ElkGraphLayoutTransferrer ) applyGraph (
arena *LGraphArena ,
graphID LGraphID ,
seen map [LGraphID ]bool ,
) error {
if seen [graphID ] {
return nil
}
seen [graphID ] = true
graph := arena .Graph (graphID )
origin , found := basegraph .TryGet (&graph .Properties , OriginalElement )
if !found || origin .Kind != OriginNode {
return fmt .Errorf ("layered: graph %d has no original parent node" , graphID )
}
offset := graph .Offset .Add (geometry .Point {X : graph .Padding .Left , Y : graph .Padding .Top })
processed := make (map [LNodeID ]bool )
for _ , nodeID := range arena .GraphAllNodes (graphID ) {
if processed [nodeID ] {
continue
}
processed [nodeID ] = true
nodeOrigin , hasOrigin := basegraph .TryGet (&arena .Node (nodeID ).Properties , OriginalElement )
if hasOrigin && nodeOrigin .Kind == OriginNode {
transferrer .applyNode (arena , nodeID , nodeOrigin .Node , offset )
} else if hasOrigin && nodeOrigin .Kind == OriginPort && graph .ParentNode == nil {
port := transferrer .Model .Port (nodeOrigin .Port )
position := arena .ExternalPortPosition (
graphID , nodeID , port .Shape .Width , port .Shape .Height )
port .Shape .SetLocation (position .X , position .Y )
}
if arena .Node (nodeID ).NestedGraph != nil {
if err := transferrer .applyGraph (arena , *arena .Node (nodeID ).NestedGraph , seen ); err != nil {
return err
}
}
}
if graph .ParentNode == nil {
transferrer .applyParentNodeLayout (arena , graphID , origin .Node )
}
return nil
}
func (transferrer *ElkGraphLayoutTransferrer ) applyParentNodeLayout (
arena *LGraphArena ,
graphID LGraphID ,
originalID basegraph .NodeID ,
) {
graph := arena .Graph (graphID )
original := transferrer .Model .Node (originalID )
sizeConstraints := basegraph .Get (&original .Properties , core .NodeSizeConstraints )
sizeConstraintsIncludedPortLabels := containsString (sizeConstraints , "PORT_LABELS" )
size := arena .GraphActualSize (graphID )
graphProperties := basegraph .Get (&graph .Properties , GraphProperties )
if graphProperties &GraphPropertyExternalPorts != 0 {
basegraph .Set (&original .Properties , core .PortConstraints , "FIXED_POS" )
elkutil .ResizeNode (transferrer .Model , originalID , size .X , size .Y , false , true )
} else if !basegraph .Get (&original .Properties , basicoptions .NodeSizeFixedGraphSize ) {
elkutil .ResizeNode (transferrer .Model , originalID , size .X , size .Y , true , true )
}
if sizeConstraintsIncludedPortLabels {
basegraph .Set (&original .Properties , core .NodeSizeConstraints , []string {"PORT_LABELS" })
} else {
basegraph .Set (&original .Properties , core .NodeSizeConstraints , []string {})
}
}
func containsString(values []string , value string ) bool {
for _ , candidate := range values {
if candidate == value {
return true
}
}
return false
}
func (transferrer *ElkGraphLayoutTransferrer ) applyNode (
arena *LGraphArena ,
nodeID LNodeID ,
originalID basegraph .NodeID ,
offset geometry .Point ,
) {
node := arena .Node (nodeID )
original := transferrer .Model .Node (originalID )
original .Shape .SetLocation (node .Position .X +offset .X , node .Position .Y +offset .Y )
original .Shape .SetDimensions (node .Size .X , node .Size .Y )
basegraph .Set (&original .Properties , LayerIDOption ,
func () int {
if node .Layer == nil {
return -1
}
return arena .LayerIndex (*node .Layer )
}())
basegraph .Set (&original .Properties , PositionIDOption , arena .NodeIndexInLayer (nodeID ))
for _ , portID := range node .Ports {
origin , found := basegraph .TryGet (&arena .Port (portID ).Properties , OriginalElement )
if !found || origin .Kind != OriginPort {
continue
}
port := arena .Port (portID )
originalPort := transferrer .Model .Port (origin .Port )
originalPort .Shape .SetLocation (port .Position .X , port .Position .Y )
originalPort .Shape .SetDimensions (port .Size .X , port .Size .Y )
basegraph .Set (&originalPort .Properties , core .PortSide , port .Side .String ())
if !fixedPortLabelPlacement (&node .Properties ) {
for _ , labelID := range port .Labels {
transferrer .applyLabel (arena , labelID , geometry .Point {})
}
}
}
nodeHasLabelPlacement := nonemptyPlacement (&node .Properties , nodespacing .NodeLabelsPlacement .ID )
for _ , labelID := range node .Labels {
if nodeHasLabelPlacement || nonemptyPlacement (
&arena .Label (labelID ).Properties ,
nodespacing .NodeLabelsPlacement .ID ,
) {
transferrer .applyLabel (arena , labelID , geometry .Point {})
}
}
}
func nonemptyPlacement(properties *basegraph .PropertyMap , id string ) bool {
value , found := properties .Raw (id )
if !found {
return false
}
switch typed := value .(type ) {
case []string :
return len (typed ) > 0
case string :
return strings .TrimSpace (typed ) != ""
default :
return false
}
}
func fixedPortLabelPlacement(properties *basegraph .PropertyMap ) bool {
value , found := properties .Raw (nodespacing .PortLabelsPlacement .ID )
if !found {
return false
}
switch typed := value .(type ) {
case []string :
for _ , item := range typed {
if strings .EqualFold (item , "FIXED" ) {
return true
}
}
case string :
for _ , field := range strings .FieldsFunc (typed , func (r rune ) bool {
return r == ' ' || r == ',' || r == '[' || r == ']'
}) {
if strings .EqualFold (field , "FIXED" ) {
return true
}
}
}
return false
}
func (transferrer *ElkGraphLayoutTransferrer ) applyLabel (
arena *LGraphArena ,
labelID LLabelID ,
offset geometry .Point ,
) {
transferrer .applyLabelInCoordinateSystem (arena , labelID , offset , geometry .Point {})
}
func (transferrer *ElkGraphLayoutTransferrer ) applyLabelInCoordinateSystem (
arena *LGraphArena ,
labelID LLabelID ,
offset geometry .Point ,
containerOrigin geometry .Point ,
) {
origin , found := basegraph .TryGet (&arena .Label (labelID ).Properties , OriginalElement )
if !found || origin .Kind != OriginLabel {
return
}
label := arena .Label (labelID )
original := transferrer .Model .Label (origin .Label )
original .Shape .SetDimensions (label .Size .X , label .Size .Y )
position := label .Position .Add (offset ).Sub (containerOrigin )
original .Shape .SetLocation (position .X , position .Y )
}
func (transferrer *ElkGraphLayoutTransferrer ) applyEdge (
arena *LGraphArena ,
edgeID LEdgeID ,
originalID basegraph .EdgeID ,
) {
edge := arena .Edge (edgeID )
if edge .Source == nil || edge .Target == nil {
return
}
original := transferrer .Model .Edge (originalID )
if len (original .Sources ) == 0 || len (original .Targets ) == 0 {
return
}
sourceNode := arena .EdgeSourceNode (edgeID )
targetNode := arena .EdgeTargetNode (edgeID )
descendantTarget := arena .IsDescendant (targetNode , sourceNode ) &&
arena .Node (sourceNode ).NestedGraph != nil
layoutGraph := arena .NodeGraph (sourceNode )
if descendantTarget {
layoutGraph = *arena .Node (sourceNode ).NestedGraph
}
baseOffset , edgeOffset := transferrer .edgeLayoutOffsets (arena , edgeID , layoutGraph )
sourcePort := arena .Port (*edge .Source )
targetPort := arena .Port (*edge .Target )
var sourcePoint geometry .Point
if descendantTarget {
sourcePoint = sourcePort .Position .Add (sourcePort .Anchor ).Sub (baseOffset )
} else {
sourcePoint = arena .Node (sourceNode ).Position .
Add (sourcePort .Position ).
Add (sourcePort .Anchor )
}
sourcePoint = sourcePoint .Add (edgeOffset )
targetPoint := arena .Node (targetNode ).Position .
Add (targetPort .Position ).
Add (targetPort .Anchor )
if targetOffset , found := basegraph .TryGet (&edge .Properties , TargetOffset ); found {
targetPoint = targetPoint .Add (targetOffset )
}
targetPoint = targetPoint .Add (edgeOffset )
sectionID := transferrer .Model .FirstSection (originalID , true )
section := transferrer .Model .Section (sectionID )
section .Start = sourcePoint
section .End = targetPoint
section .StartPresent , section .EndPresent = true , true
section .IncomingShape = copyShapeID (&original .Sources [0 ])
section .OutgoingShape = copyShapeID (&original .Targets [0 ])
section .BendPoints = make (geometry .Chain , len (edge .BendPoints ))
for index , point := range edge .BendPoints {
section .BendPoints [index ] = point .Add (edgeOffset )
}
section .BendPointsPresent = len (section .BendPoints ) > 0
for _ , labelID := range edge .Labels {
transferrer .applyLabel (arena , labelID , edgeOffset )
}
if junctions , found := basegraph .TryGet (&edge .Properties , JunctionPoints ); found && len (junctions ) > 0 {
translated := make (geometry .Chain , len (junctions ))
for index , point := range junctions {
translated [index ] = point .Add (edgeOffset )
}
basegraph .Set (&original .Properties , core .JunctionPoints , translated )
} else {
basegraph .Unset (&original .Properties , core .JunctionPoints )
}
}
func (transferrer *ElkGraphLayoutTransferrer ) edgeLayoutOffsets (
arena *LGraphArena ,
edgeID LEdgeID ,
layoutGraph LGraphID ,
) (geometry .Point , geometry .Point ) {
graph := arena .Graph (layoutGraph )
baseOffset := graph .Offset .Add (
geometry .Point {X : graph .Padding .Left , Y : graph .Padding .Top })
hierarchicalOffset := geometry .Point {}
targetGraph , found := basegraph .TryGet (
&arena .Edge (edgeID ).Properties ,
CoordinateSystemOrigin ,
)
if found {
currentGraph := arena .NodeGraph (arena .EdgeSourceNode (edgeID ))
for currentGraph != targetGraph {
current := arena .Graph (currentGraph )
if current .ParentNode == nil {
break
}
representingNode := *current .ParentNode
hierarchicalOffset = hierarchicalOffset .Add (
arena .Node (representingNode ).Position )
currentGraph = arena .NodeGraph (representingNode )
parentGraph := arena .Graph (currentGraph )
hierarchicalOffset = hierarchicalOffset .
Add (parentGraph .Offset ).
Add (geometry .Point {
X : parentGraph .Padding .Left ,
Y : parentGraph .Padding .Top ,
})
}
}
return baseOffset , baseOffset .Add (hierarchicalOffset )
}
func copyShapeID(value *basegraph .ShapeID ) *basegraph .ShapeID {
if value == nil {
return nil
}
copy := *value
return ©
}
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 .