|
1 | 1 | import { DiagramModel, PointModel } from '@projectstorm/react-diagrams-core'; |
2 | 2 | import * as dagre from 'dagre'; |
3 | | -import * as _ from 'lodash'; |
4 | 3 | import { GraphLabel } from 'dagre'; |
| 4 | +import * as _ from 'lodash'; |
5 | 5 | import { Point } from '@projectstorm/geometry'; |
6 | 6 |
|
7 | 7 | export interface DagreEngineOptions { |
8 | 8 | graph?: GraphLabel; |
9 | 9 | /** |
10 | | - * Will also layout links |
| 10 | + * Will also re-layout links |
11 | 11 | */ |
12 | 12 | includeLinks?: boolean; |
| 13 | + nodeMargin?: number; |
13 | 14 | } |
14 | 15 |
|
15 | 16 | export class DagreEngine { |
@@ -68,4 +69,184 @@ export class DagreEngine { |
68 | 69 | }); |
69 | 70 | } |
70 | 71 | } |
| 72 | + |
| 73 | + /** |
| 74 | + * TODO cleanup this method into smaller methods |
| 75 | + */ |
| 76 | + public refreshLinks(diagram: DiagramModel) { |
| 77 | + const { nodeMargin } = this.options; |
| 78 | + const nodes = diagram.getNodes(); |
| 79 | + const links = diagram.getLinks(); |
| 80 | + let maxChunkRowIndex = -1; |
| 81 | + // build the chunk matrix |
| 82 | + const chunks: { [id: number]: { [id: number]: boolean } } = {}; // true: occupied, false: blank |
| 83 | + const NodeXColumnIndexDictionary: { [id: number]: number } = {}; |
| 84 | + let verticalLines: number[] = []; |
| 85 | + _.forEach(nodes, (node) => { |
| 86 | + // find vertical lines. vertical lines go through maximum number of nodes located under each other. |
| 87 | + const nodeColumnCenter = node.getX() + node.width / 2; |
| 88 | + if ( |
| 89 | + _.every(verticalLines, (vLine) => { |
| 90 | + return Math.abs(nodeColumnCenter - vLine) > nodeMargin; |
| 91 | + }) |
| 92 | + ) { |
| 93 | + verticalLines.push(nodeColumnCenter); |
| 94 | + } |
| 95 | + }); |
| 96 | + |
| 97 | + // sort chunk columns |
| 98 | + verticalLines = verticalLines.sort((a, b) => a - b); |
| 99 | + _.forEach(verticalLines, (line, index) => { |
| 100 | + chunks[index] = {}; |
| 101 | + chunks[index + 0.5] = {}; |
| 102 | + }); |
| 103 | + |
| 104 | + // set occupied chunks |
| 105 | + _.forEach(nodes, (node) => { |
| 106 | + const nodeColumnCenter = node.getX() + node.width / 2; |
| 107 | + const startChunkIndex = Math.floor(node.getY() / nodeMargin); |
| 108 | + const endChunkIndex = Math.floor((node.getY() + node.height) / nodeMargin); |
| 109 | + // find max ChunkRowIndex |
| 110 | + if (endChunkIndex > maxChunkRowIndex) maxChunkRowIndex = endChunkIndex; |
| 111 | + const nodeColumnIndex = _.findIndex(verticalLines, (vLine) => { |
| 112 | + return Math.abs(nodeColumnCenter - vLine) <= nodeMargin; |
| 113 | + }); |
| 114 | + _.forEach(_.range(startChunkIndex, endChunkIndex + 1), (chunkIndex) => { |
| 115 | + chunks[nodeColumnIndex][chunkIndex] = true; |
| 116 | + }); |
| 117 | + NodeXColumnIndexDictionary[node.getX()] = nodeColumnIndex; |
| 118 | + }); |
| 119 | + |
| 120 | + // sort links based on their distances |
| 121 | + const edges = _.map(links, (link) => { |
| 122 | + if (link.getSourcePort() && link.getTargetPort()) { |
| 123 | + const source = link.getSourcePort().getNode(); |
| 124 | + const target = link.getTargetPort().getNode(); |
| 125 | + const sourceIndex = NodeXColumnIndexDictionary[source.getX()]; |
| 126 | + const targetIndex = NodeXColumnIndexDictionary[target.getX()]; |
| 127 | + |
| 128 | + return sourceIndex > targetIndex |
| 129 | + ? { |
| 130 | + link, |
| 131 | + sourceIndex, |
| 132 | + sourceY: source.getY() + source.height / 2, |
| 133 | + source, |
| 134 | + targetIndex, |
| 135 | + targetY: target.getY() + source.height / 2, |
| 136 | + target |
| 137 | + } |
| 138 | + : { |
| 139 | + link, |
| 140 | + sourceIndex: targetIndex, |
| 141 | + sourceY: target.getY() + target.height / 2, |
| 142 | + source: target, |
| 143 | + targetIndex: sourceIndex, |
| 144 | + targetY: source.getY() + source.height / 2, |
| 145 | + target: source |
| 146 | + }; |
| 147 | + } |
| 148 | + }); |
| 149 | + const sortedEdges = _.sortBy(edges, (link) => { |
| 150 | + return Math.abs(link.targetIndex - link.sourceIndex); |
| 151 | + }); |
| 152 | + |
| 153 | + // set link points |
| 154 | + if (this.options.includeLinks) { |
| 155 | + _.forEach(sortedEdges, (edge) => { |
| 156 | + const link = diagram.getLink(edge.link.getID()); |
| 157 | + // re-draw |
| 158 | + if (Math.abs(edge.sourceIndex - edge.targetIndex) > 1) { |
| 159 | + // get the length of link in column |
| 160 | + const columns = _.range(edge.sourceIndex - 1, edge.targetIndex); |
| 161 | + |
| 162 | + const chunkIndex = Math.floor(edge.sourceY / nodeMargin); |
| 163 | + const targetChunkIndex = Math.floor(edge.targetY / nodeMargin); |
| 164 | + |
| 165 | + // check upper paths |
| 166 | + let northCost = 1; |
| 167 | + let aboveRowIndex = chunkIndex; |
| 168 | + for (; aboveRowIndex >= 0; aboveRowIndex--, northCost++) { |
| 169 | + if ( |
| 170 | + _.every(columns, (columnIndex) => { |
| 171 | + return !( |
| 172 | + chunks[columnIndex][aboveRowIndex] || |
| 173 | + chunks[columnIndex + 0.5][aboveRowIndex] || |
| 174 | + chunks[columnIndex - 0.5][aboveRowIndex] |
| 175 | + ); |
| 176 | + }) |
| 177 | + ) { |
| 178 | + break; |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + // check lower paths |
| 183 | + let southCost = 0; |
| 184 | + let belowRowIndex = chunkIndex; |
| 185 | + for (; belowRowIndex <= maxChunkRowIndex; belowRowIndex++, southCost++) { |
| 186 | + if ( |
| 187 | + _.every(columns, (columnIndex) => { |
| 188 | + return !( |
| 189 | + chunks[columnIndex][belowRowIndex] || |
| 190 | + chunks[columnIndex + 0.5][belowRowIndex] || |
| 191 | + chunks[columnIndex - 0.5][belowRowIndex] |
| 192 | + ); |
| 193 | + }) |
| 194 | + ) { |
| 195 | + break; |
| 196 | + } |
| 197 | + } |
| 198 | + // pick the cheapest path |
| 199 | + const pathRowIndex = |
| 200 | + southCost + (belowRowIndex - targetChunkIndex) < northCost + (targetChunkIndex - aboveRowIndex) |
| 201 | + ? belowRowIndex + 1 |
| 202 | + : aboveRowIndex - 1; |
| 203 | + |
| 204 | + // Finally update the link points |
| 205 | + const points = [link.getFirstPoint()]; |
| 206 | + points.push( |
| 207 | + new PointModel({ |
| 208 | + link: link, |
| 209 | + position: new Point( |
| 210 | + (verticalLines[columns[0]] + verticalLines[columns[0] + 1]) / 2, |
| 211 | + (pathRowIndex + 0.5) * nodeMargin |
| 212 | + ) |
| 213 | + }) |
| 214 | + ); |
| 215 | + |
| 216 | + _.forEach(columns, (column) => { |
| 217 | + points.push( |
| 218 | + new PointModel({ |
| 219 | + link: link, |
| 220 | + position: new Point(verticalLines[column], (pathRowIndex + 0.5) * nodeMargin) |
| 221 | + }) |
| 222 | + ); |
| 223 | + points.push( |
| 224 | + new PointModel({ |
| 225 | + link: link, |
| 226 | + position: new Point( |
| 227 | + (verticalLines[column] + verticalLines[column - 1]) / 2, |
| 228 | + (pathRowIndex + 0.5) * nodeMargin |
| 229 | + ) |
| 230 | + }) |
| 231 | + ); |
| 232 | + chunks[column][pathRowIndex] = true; |
| 233 | + chunks[column][pathRowIndex + 1] = true; |
| 234 | + chunks[column + 0.5][pathRowIndex] = true; |
| 235 | + chunks[column + 0.5][pathRowIndex + 1] = true; |
| 236 | + }); |
| 237 | + link.setPoints(points.concat(link.getLastPoint())); |
| 238 | + } else { |
| 239 | + // refresh |
| 240 | + link.setPoints([link.getFirstPoint(), link.getLastPoint()]); |
| 241 | + const columnIndex = (edge.sourceIndex + edge.targetIndex) / 2; |
| 242 | + if (!chunks[columnIndex]) { |
| 243 | + chunks[columnIndex] = {}; |
| 244 | + } |
| 245 | + const rowIndex = Math.floor((edge.sourceY + edge.targetY) / 2 / nodeMargin); |
| 246 | + chunks[columnIndex][rowIndex] = true; |
| 247 | + chunks[columnIndex][rowIndex + 1] = true; |
| 248 | + } |
| 249 | + }); |
| 250 | + } |
| 251 | + } |
71 | 252 | } |
0 commit comments