package spore
import (
"fmt"
"math"
"math/rand"
"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"
"github.com/d2lang/elk-go/internal/graph"
)
type importer struct {
model *graph .Model
parent graph .NodeID
graph *sporeGraph
spacing float64
}
func newImporter(model *graph .Model , parent graph .NodeID ) *importer {
return &importer {model : model , parent : parent }
}
func (i *importer ) importGraph () (*sporeGraph , error ) {
parentNode := i .model .Node (i .parent )
nodespacing .NewNodeMarginCalculator ().Process (i .model , i .parent )
i .spacing = graph .Get (&parentNode .Properties , SpacingNodeNode )
g := &sporeGraph {
treeConstruction : graph .Get (&parentNode .Properties , TreeConstruction ),
compaction : graph .Get (&parentNode .Properties , Compaction ),
orthogonal : graph .Get (&parentNode .Properties , Orthogonal ),
}
i .graph = g
for _ , nodeID := range parentNode .Children {
elkNode := i .model .Node (nodeID )
halfWidth , halfHeight := elkNode .Shape .Width /2 , elkNode .Shape .Height /2
point := geometry .Point {X : elkNode .Shape .X + halfWidth , Y : elkNode .Shape .Y + halfHeight }
for i .containsPoint (point ) {
point .X += (rand .Float64 () - 0.5 ) * 0.001
point .Y += (rand .Float64 () - 0.5 ) * 0.001
}
original := &vertex {point : point }
margin := graph .Get (&elkNode .Properties , nodespacing .Margins )
rect := geometry .Rectangle {
X : point .X - halfWidth - i .spacing /2 - margin .Left ,
Y : point .Y - halfHeight - i .spacing /2 - margin .Top ,
Width : elkNode .Shape .Width + i .spacing + margin .Horizontal (),
Height : elkNode .Shape .Height + i .spacing + margin .Vertical (),
}
g .vertices = append (g .vertices , newNode (original , rect , nodeID ))
}
if len (g .vertices ) == 0 {
return g , nil
}
switch graph .Get (&parentNode .Properties , RootSelection ) {
case RootFixed :
preferred , present := graph .TryGet (&parentNode .Properties , PreferredRoot )
if !present {
g .preferredRoot = g .vertices [0 ]
} else {
for _ , candidate := range g .vertices {
if i .model .Node (candidate .origin ).IdentifierPresent && i .model .Node (candidate .origin ).Identifier == preferred {
g .preferredRoot = candidate
}
}
}
case RootCenter :
center := geometry .Point {
X : parentNode .Shape .X + parentNode .Shape .Width /2 ,
Y : parentNode .Shape .Y + parentNode .Shape .Height /2 ,
}
closest := math .Inf (1 )
for _ , candidate := range g .vertices {
distance := candidate .original .point .Distance (center )
if distance < closest {
closest = distance
g .preferredRoot = candidate
}
}
default :
return nil , fmt .Errorf ("no SPOrE root selection implementation for %q" , graph .Get (&parentNode .Properties , RootSelection ))
}
switch graph .Get (&parentNode .Properties , SpanningTreeCost ) {
case CostCenterDistance :
g .cost = func (edge *tEdge ) float64 { return edge .u .point .Distance (edge .v .point ) }
case CostCircleUnderlap :
g .cost = func (edge *tEdge ) float64 {
first , second := i .nodeFor (edge .u ), i .nodeFor (edge .v )
return edge .u .point .Distance (edge .v .point ) -
edge .u .point .Distance (first .rect .Position ()) -
edge .v .point .Distance (second .rect .Position ())
}
case CostRectangleUnderlap :
g .cost = func (edge *tEdge ) float64 { return i .nodeFor (edge .u ).underlap (i .nodeFor (edge .v )) }
case CostInvertedOverlap :
g .cost = func (edge *tEdge ) float64 {
first , second := i .nodeFor (edge .u ), i .nodeFor (edge .v )
distance := shortestDistance (first .rect , second .rect )
if distance >= 0 {
return distance
}
centerDistance := second .rect .Center ().Sub (first .rect .Center ()).Length ()
return -(overlap (first .rect , second .rect ) - 1 ) * centerDistance
}
case CostMinimumRootDistance :
g .cost = func (edge *tEdge ) float64 {
return math .Min (
edge .u .point .Distance (g .preferredRoot .vertex ),
edge .v .point .Distance (g .preferredRoot .vertex ),
)
}
default :
return nil , fmt .Errorf ("no SPOrE spanning-tree cost implementation for %q" , graph .Get (&parentNode .Properties , SpanningTreeCost ))
}
return g , nil
}
func (i *importer ) containsPoint (point geometry .Point ) bool {
for _ , candidate := range i .graph .vertices {
if candidate .original .point == point {
return true
}
}
return false
}
func (i *importer ) nodeFor (wanted *vertex ) *node {
for _ , candidate := range i .graph .vertices {
if candidate .original == wanted {
return candidate
}
}
return nil
}
func (i *importer ) updateGraph () {
i .graph .edges = nil
i .graph .tree = nil
for _ , candidate := range i .graph .vertices {
candidate .original = &vertex {point : candidate .rect .Center ()}
}
}
func (i *importer ) applyPositions () {
if len (i .graph .vertices ) == 0 {
return
}
minimumX , minimumY := math .Inf (1 ), math .Inf (1 )
maximumX , maximumY := math .Inf (-1 ), math .Inf (-1 )
for _ , candidate := range i .graph .vertices {
elkNode := i .model .Node (candidate .origin )
elkNode .Shape .SetLocation (candidate .rect .X , candidate .rect .Y )
minimumX = math .Min (minimumX , elkNode .Shape .X )
minimumY = math .Min (minimumY , elkNode .Shape .Y )
maximumX = math .Max (maximumX , elkNode .Shape .X +elkNode .Shape .Width )
maximumY = math .Max (maximumY , elkNode .Shape .Y +elkNode .Shape .Height )
}
padding := graph .Get (&i .model .Node (i .parent ).Properties , Padding )
elkutil .ResizeNode (
i .model ,
i .parent ,
maximumX -minimumX +padding .Horizontal (),
maximumY -minimumY +padding .Vertical (),
true ,
true ,
)
elkutil .Translate (i .model , i .parent , -minimumX +padding .Left , -minimumY +padding .Top )
i .routeEdges ()
}
func (i *importer ) routeEdges () {
for _ , edgeID := range i .model .Node (i .parent ).ContainedEdges {
edge := i .model .Edge (edgeID )
if len (edge .Sources ) == 0 || len (edge .Targets ) == 0 {
continue
}
if len (edge .Sections ) == 0 {
i .model .AddSection (edgeID )
}
section := i .model .Section (edge .Sections [0 ])
source := i .model .Node (i .model .ShapeNode (edge .Sources [0 ]))
target := i .model .Node (i .model .ShapeNode (edge .Targets [0 ]))
start := source .Shape .Rectangle ().Center ()
end := target .Shape .Rectangle ().Center ()
start = start .Add (clipVector (end .Sub (start ), source .Shape .Width , source .Shape .Height ))
end = end .Add (clipVector (start .Sub (end ), target .Shape .Width , target .Shape .Height ))
section .Start , section .End = start , end
section .StartPresent , section .EndPresent = true , true
}
}
func runUnderlying(model *graph .Model , parent graph .NodeID , monitor core .ProgressMonitor , algorithms *core .AlgorithmRegistry ) error {
properties := &model .Node (parent ).Properties
requested , present := graph .TryGet (properties , UnderlyingLayoutAlgorithm )
if !present || algorithms == nil {
return nil
}
algorithm := algorithms .BySuffix (requested )
if algorithm == nil {
return nil
}
provider := algorithm .Create ()
if provider == nil {
return fmt .Errorf ("underlying layout algorithm %q returned a nil provider" , requested )
}
return provider .Layout (model , parent , monitor .Subtask (1 ))
}
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 .