|
| 1 | +package layout |
| 2 | + |
| 3 | +import ( |
| 4 | + "github.com/docToolchain/Bausteinsicht/internal/model" |
| 5 | +) |
| 6 | + |
| 7 | +// HierarchicalLayout computes layer assignments via longest-path algorithm, |
| 8 | +// then positions elements in layers with horizontal alignment. |
| 9 | +type HierarchicalLayout struct { |
| 10 | + model *model.BausteinsichtModel |
| 11 | + rankDir string // TB or LR |
| 12 | + spacing float64 |
| 13 | + layerGap float64 |
| 14 | +} |
| 15 | + |
| 16 | +// NewHierarchicalLayout creates a hierarchical layout engine. |
| 17 | +func NewHierarchicalLayout(m *model.BausteinsichtModel, rankDir string) *HierarchicalLayout { |
| 18 | + if rankDir == "" { |
| 19 | + rankDir = "TB" |
| 20 | + } |
| 21 | + return &HierarchicalLayout{ |
| 22 | + model: m, |
| 23 | + rankDir: rankDir, |
| 24 | + spacing: 20, // pixels between elements in same layer |
| 25 | + layerGap: 100, // pixels between layers |
| 26 | + } |
| 27 | +} |
| 28 | + |
| 29 | +// Compute calculates positions for all elements. |
| 30 | +func (h *HierarchicalLayout) Compute() LayoutResult { |
| 31 | + outgoing := h.buildOutgoingMap() |
| 32 | + |
| 33 | + // Assign layers using longest-path algorithm |
| 34 | + layers := h.assignLayers(outgoing) |
| 35 | + |
| 36 | + // Position elements based on layers |
| 37 | + positions := h.positionElements(layers) |
| 38 | + |
| 39 | + return LayoutResult{ |
| 40 | + Positions: positions, |
| 41 | + Algorithm: Hierarchical, |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +// buildOutgoingMap creates a map of outgoing relationships per element. |
| 46 | +func (h *HierarchicalLayout) buildOutgoingMap() map[string][]string { |
| 47 | + outgoing := make(map[string][]string) |
| 48 | + for _, rel := range h.model.Relationships { |
| 49 | + outgoing[rel.From] = append(outgoing[rel.From], rel.To) |
| 50 | + } |
| 51 | + return outgoing |
| 52 | +} |
| 53 | + |
| 54 | +// assignLayers assigns each element to a layer using longest-path algorithm. |
| 55 | +func (h *HierarchicalLayout) assignLayers(outgoing map[string][]string) map[int][]string { |
| 56 | + flat, _ := model.FlattenElements(h.model) |
| 57 | + |
| 58 | + // Compute longest path from each node |
| 59 | + depths := make(map[string]int) |
| 60 | + for id := range flat { |
| 61 | + depths[id] = h.longestPath(id, outgoing, make(map[string]bool)) |
| 62 | + } |
| 63 | + |
| 64 | + // Group elements by layer |
| 65 | + layers := make(map[int][]string) |
| 66 | + maxLayer := 0 |
| 67 | + for id, depth := range depths { |
| 68 | + layers[depth] = append(layers[depth], id) |
| 69 | + if depth > maxLayer { |
| 70 | + maxLayer = depth |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + return layers |
| 75 | +} |
| 76 | + |
| 77 | +// longestPath computes longest outgoing path from a node (memoized). |
| 78 | +func (h *HierarchicalLayout) longestPath(id string, outgoing map[string][]string, visited map[string]bool) int { |
| 79 | + if visited[id] { |
| 80 | + return 0 // cycle detected, break here |
| 81 | + } |
| 82 | + |
| 83 | + targets := outgoing[id] |
| 84 | + if len(targets) == 0 { |
| 85 | + return 0 |
| 86 | + } |
| 87 | + |
| 88 | + visited[id] = true |
| 89 | + maxDepth := 0 |
| 90 | + for _, target := range targets { |
| 91 | + depth := h.longestPath(target, outgoing, visited) |
| 92 | + if depth > maxDepth { |
| 93 | + maxDepth = depth |
| 94 | + } |
| 95 | + } |
| 96 | + delete(visited, id) |
| 97 | + |
| 98 | + return maxDepth + 1 |
| 99 | +} |
| 100 | + |
| 101 | +// positionElements places elements horizontally within each layer. |
| 102 | +func (h *HierarchicalLayout) positionElements(layers map[int][]string) map[string]ElementPosition { |
| 103 | + positions := make(map[string]ElementPosition) |
| 104 | + |
| 105 | + for layer, ids := range layers { |
| 106 | + // Default sizes |
| 107 | + elemWidth := 160.0 |
| 108 | + elemHeight := 60.0 |
| 109 | + |
| 110 | + // Calculate layer positions |
| 111 | + var x, y float64 |
| 112 | + if h.rankDir == "TB" { |
| 113 | + // Top-to-bottom: layer determines Y, elements spread horizontally |
| 114 | + y = float64(layer) * h.layerGap |
| 115 | + x = 50.0 |
| 116 | + } else { |
| 117 | + // Left-to-right: layer determines X, elements spread vertically |
| 118 | + x = float64(layer) * h.layerGap |
| 119 | + y = 50.0 |
| 120 | + } |
| 121 | + |
| 122 | + // Position elements in this layer |
| 123 | + for i, id := range ids { |
| 124 | + elemX, elemY := x, y |
| 125 | + if h.rankDir == "TB" { |
| 126 | + elemX = x + float64(i)*(elemWidth+h.spacing) |
| 127 | + } else { |
| 128 | + elemY = y + float64(i)*(elemHeight+h.spacing) |
| 129 | + } |
| 130 | + |
| 131 | + positions[id] = ElementPosition{ |
| 132 | + ID: id, |
| 133 | + X: elemX, |
| 134 | + Y: elemY, |
| 135 | + Width: elemWidth, |
| 136 | + Height: elemHeight, |
| 137 | + Layer: layer, |
| 138 | + } |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + return positions |
| 143 | +} |
0 commit comments