Skip to content

Commit 5b209c0

Browse files
JakobVogelsangRob Tjalma
andauthored
refactor(SingleLineDiagram): improvements in perormance and visualizasion (#403)
* feat(SingleLineDiagram): add ortho-connector pathfinding algorithm * refactor(SingleLineDiagram): move to new ortho-connecotr * test(SingleLineDiagram): fix and add unit tests * Small unit test fix * refactor(sld-drawing): rebase to main * test: disable invalid SingleLineDiagram.test.ts * test: remove invalid SingleLineDiagram test * refactor(ortho-connector): improve performance * test(connectivitynode): add snapshot test * test(wizards/terminal): add snapshot test * fix(singlelinediagram/foundation): fail save connectivitynode allocation Co-authored-by: Rob Tjalma <[email protected]>
1 parent e3408d2 commit 5b209c0

File tree

13 files changed

+1307
-1280
lines changed

13 files changed

+1307
-1280
lines changed

public/js/ortho-connector.ts

Lines changed: 0 additions & 885 deletions
This file was deleted.

src/editors/SingleLineDiagram.ts

Lines changed: 98 additions & 144 deletions
Large diffs are not rendered by default.

src/editors/singlelinediagram/foundation.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
* A point is a position containing a x and a y within a SCL file.
33
*/
44
export interface Point {
5-
x: number | undefined;
6-
y: number | undefined;
5+
x: number;
6+
y: number;
77
}
88

9+
/** Scope factor: the ConnectivityNode allocation algorithm works better with a scale factor which is bigger than 1. */
10+
const COORDINATES_SCALE_FACTOR = 2;
11+
912
/**
1013
* Extract the 'name' attribute from the given XML element.
1114
* @param element - The element to extract name from.
@@ -52,8 +55,8 @@ export function getRelativeCoordinates(element: Element): Point {
5255
);
5356

5457
return {
55-
x: x ? parseInt(x) : 0,
56-
y: y ? parseInt(y) : 0,
58+
x: x ? parseInt(x) * COORDINATES_SCALE_FACTOR : 0,
59+
y: y ? parseInt(y) * COORDINATES_SCALE_FACTOR : 0,
5760
};
5861
}
5962

@@ -148,6 +151,9 @@ export function calculateConnectivityNodeCoordinates(
148151
totalY += y!;
149152
});
150153

154+
if (nrOfConnections === 0) return { x: 0, y: 0 };
155+
if (nrOfConnections === 1) return { x: totalX + 1, y: totalY + 1 };
156+
151157
return {
152158
x: Math.round(totalX / nrOfConnections),
153159
y: Math.round(totalY / nrOfConnections),
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
interface Point {
2+
x: number;
3+
y: number;
4+
}
5+
6+
interface Adjacent {
7+
point: Point;
8+
edgeWeight: number;
9+
}
10+
11+
interface GraphNode {
12+
point: Point;
13+
adjacent: Adjacent[];
14+
dist: number;
15+
path: Point[];
16+
}
17+
18+
function distance(a: Point, b: Point): number {
19+
return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
20+
}
21+
22+
function isChangedDirection(a: GraphNode, b: GraphNode): boolean {
23+
if (a.path.length === 0) return false;
24+
25+
const commingX2 = a.point.x;
26+
const commingX1 = a.path[a.path.length - 1].x;
27+
const commingHorizontal = commingX2 - commingX1 ? false : true;
28+
29+
const goingHorizontal = a.point.x - b.point.x ? false : true;
30+
31+
return commingHorizontal !== goingHorizontal;
32+
}
33+
34+
function filterUnchangedDirection(path: Point[]): Point[] {
35+
return path.filter((p, i, v) => {
36+
if (i === 0 || i === v.length - 1) return true;
37+
38+
const commingDirection =
39+
v[i].x - v[i - 1].x !== 0 ? 'horizontal' : 'vertical';
40+
const goingDirection =
41+
v[i + 1].x - v[i].x !== 0 ? 'horizontal' : 'vertical';
42+
43+
return commingDirection === goingDirection ? false : true;
44+
});
45+
}
46+
47+
function calculateMinimumDistance(
48+
adjacent: GraphNode,
49+
edgeWeigh: number,
50+
parentNode: GraphNode
51+
) {
52+
const sourceDistance = parentNode.dist;
53+
54+
const changingDirection = isChangedDirection(parentNode, adjacent);
55+
const extraWeigh = changingDirection ? Math.pow(edgeWeigh + 1, 2) : 0;
56+
57+
if (sourceDistance + edgeWeigh + extraWeigh < adjacent.dist) {
58+
adjacent.dist = sourceDistance + edgeWeigh + extraWeigh;
59+
const shortestPath: Point[] = [...parentNode.path];
60+
shortestPath.push(parentNode.point);
61+
adjacent.path = shortestPath;
62+
}
63+
}
64+
65+
function getLowestDistanceGraphNode(
66+
unsettledNodes: Set<GraphNode>
67+
): GraphNode | null {
68+
let lowestDistance = Number.MAX_SAFE_INTEGER;
69+
let lowestDistanceNode: GraphNode | null = null;
70+
71+
for (const node of unsettledNodes)
72+
if (node.dist < lowestDistance) {
73+
lowestDistance = node.dist;
74+
lowestDistanceNode = node;
75+
}
76+
77+
return lowestDistanceNode;
78+
}
79+
80+
function dijkstra(graph: GraphNode[], start: GraphNode): Point[] {
81+
start.dist = 0;
82+
83+
const settledNodes: Set<GraphNode> = new Set();
84+
const unsettledNodes: Set<GraphNode> = new Set();
85+
86+
unsettledNodes.add(start);
87+
88+
while (unsettledNodes.size != 0) {
89+
const currentNode = getLowestDistanceGraphNode(unsettledNodes)!;
90+
unsettledNodes.delete(currentNode);
91+
92+
for (const adjacent of currentNode.adjacent) {
93+
const adjacentNode = graph.find(
94+
node =>
95+
node.point.x === adjacent.point.x && node.point.y === adjacent.point.y
96+
);
97+
const edgeWeight = adjacent.edgeWeight;
98+
if (adjacentNode && !settledNodes.has(adjacentNode)) {
99+
calculateMinimumDistance(adjacentNode, edgeWeight, currentNode);
100+
unsettledNodes.add(adjacentNode);
101+
}
102+
}
103+
settledNodes.add(currentNode);
104+
}
105+
106+
return [];
107+
}
108+
109+
function findClosestGraphNode(
110+
graph: GraphNode[],
111+
point: Point
112+
): GraphNode | undefined {
113+
const distFromGraphNodes = graph.map(
114+
node => Math.abs(point.x - node.point.x) + Math.abs(point.y - node.point.y)
115+
);
116+
117+
const minDistance = Math.min(...distFromGraphNodes);
118+
const index = distFromGraphNodes.indexOf(minDistance);
119+
120+
return graph[index];
121+
}
122+
123+
function addStartNode(graph: GraphNode[], start: Point): GraphNode | undefined {
124+
const closestToStart = findClosestGraphNode(graph, start)?.point;
125+
if (!closestToStart) return undefined;
126+
127+
const startNode = {
128+
point: start,
129+
adjacent: [
130+
{ point: closestToStart, edgeWeight: distance(start, closestToStart) },
131+
],
132+
dist: Number.MAX_SAFE_INTEGER,
133+
path: [],
134+
};
135+
graph.push(startNode);
136+
137+
return startNode;
138+
}
139+
140+
function getPath(graph: GraphNode[], start: Point, end: Point): Point[] {
141+
const startNode = addStartNode(graph, start);
142+
const closestToEnd = findClosestGraphNode(graph, end);
143+
144+
if (!startNode || !closestToEnd) return [];
145+
146+
dijkstra(graph, startNode);
147+
const shortestPath = closestToEnd.path.concat(closestToEnd.point);
148+
149+
return filterUnchangedDirection(shortestPath).concat([end]);
150+
}
151+
152+
function findGraphNode(
153+
graph: GraphNode[],
154+
x: number,
155+
y: number
156+
): GraphNode | undefined {
157+
return graph.find(node => node.point.x === x && node.point.y === y);
158+
}
159+
160+
function findAdjacent(
161+
graph: GraphNode[],
162+
currentNode: GraphNode,
163+
gridSize: number,
164+
type: string
165+
): Adjacent | null {
166+
let dX1: number;
167+
let dY1: number;
168+
169+
if (type === 'prevX') {
170+
dX1 = currentNode.point.x - gridSize;
171+
dY1 = currentNode.point.y;
172+
} else if (type === 'prevY') {
173+
dX1 = currentNode.point.x;
174+
dY1 = currentNode.point.y - gridSize;
175+
} else if (type === 'nextX') {
176+
dX1 = currentNode.point.x + gridSize;
177+
dY1 = currentNode.point.y;
178+
} else {
179+
dX1 = currentNode.point.x;
180+
dY1 = currentNode.point.y + gridSize;
181+
}
182+
183+
if (findGraphNode(graph, dX1!, dY1!)) {
184+
return {
185+
point: findGraphNode(graph, dX1!, dY1!)!.point,
186+
edgeWeight: gridSize,
187+
};
188+
}
189+
190+
return null;
191+
}
192+
193+
function createGraph(allocation: number[][], gridSize: number): GraphNode[] {
194+
const graph: GraphNode[] = [];
195+
for (let row = 0; row < allocation.length; row++)
196+
for (let col = 0; col < allocation[row].length; col++)
197+
if (allocation[row][col] === 0)
198+
graph.push({
199+
point: {
200+
x: col * gridSize + gridSize / 2,
201+
y: row * gridSize + gridSize / 2,
202+
},
203+
adjacent: [],
204+
dist: Number.MAX_SAFE_INTEGER,
205+
path: [],
206+
});
207+
208+
for (const node of graph) {
209+
const adjacents = <Adjacent[]>(
210+
['prevX', 'prevY', 'nextX', 'nextY']
211+
.map(type => findAdjacent(graph, node, gridSize, type))
212+
.filter(adjacent => adjacent)
213+
);
214+
node.adjacent = adjacents;
215+
}
216+
217+
return graph;
218+
}
219+
220+
function emptyAllocation(
221+
start: Point,
222+
end: Point,
223+
gridSize: number
224+
): (0 | 1)[][] {
225+
const maxX = start.x > end.x ? start.x : end.x;
226+
const maxY = start.y > end.y ? start.y : end.y;
227+
228+
const emptyGrid: (0 | 1)[][] = [];
229+
for (let i = 0; i <= Math.ceil(maxY / gridSize) + 1; i++) {
230+
emptyGrid[i] = [];
231+
for (let j = 0; j <= Math.ceil(maxX / gridSize) + 1; j++) {
232+
emptyGrid[i][j] = 0;
233+
}
234+
}
235+
236+
emptyGrid[Math.floor(start.y / gridSize)][Math.floor(start.x / gridSize)] = 1;
237+
emptyGrid[Math.floor(end.y / gridSize)][Math.floor(end.x / gridSize)] = 1;
238+
239+
return emptyGrid;
240+
}
241+
242+
//FIXME: This is a dirty trick to improve performance of the algorithm
243+
function trimStartEnd(start: Point, end: Point, gridSize: number): Point[] {
244+
//FIXME: Dirty hack to speed up the algorithm
245+
const minCoordX = Math.min(
246+
Math.floor(start.x / gridSize),
247+
Math.floor(end.x / gridSize)
248+
);
249+
const minCoordY = Math.min(
250+
Math.floor(start.y / gridSize),
251+
Math.floor(end.y / gridSize)
252+
);
253+
254+
const dCoordX = minCoordX > 1 ? minCoordX - 1 : 0;
255+
const dCoordY = minCoordY > 1 ? minCoordY - 1 : 0;
256+
257+
const deltaX = dCoordX * gridSize;
258+
const deltaY = dCoordY * gridSize;
259+
260+
return [
261+
{ x: start.x - deltaX, y: start.y - deltaY },
262+
{ x: end.x - deltaX, y: end.y - deltaY },
263+
];
264+
}
265+
266+
function fullPath(
267+
path: Point[],
268+
start: Point,
269+
end: Point,
270+
trimmedStart: Point,
271+
trimmedEnd: Point
272+
): Point[] {
273+
if (start === trimmedStart && end === trimmedEnd) return path;
274+
275+
const deltaX = start.x - trimmedStart.x;
276+
const deltaY = start.y - trimmedStart.y;
277+
278+
return path.map(point => {
279+
return { x: point.x + deltaX, y: point.y + deltaY };
280+
});
281+
}
282+
283+
/** Finds the shortest orthogonal path between start and end based on grid and dijkstra path finding algorithm
284+
* @param start - the position in px of the start point
285+
* @param end - the position in px of the end point
286+
* @param gridSize - grid size of the grid to rout in the orthogonal path
287+
* @param gridAllocation - optional [][] matrix to define allocated grid cells
288+
* @returns - Array of positions in px building the orthogonal path
289+
*/
290+
export function getOrthogonalPath(
291+
start: Point,
292+
end: Point,
293+
gridSize: number,
294+
gridAllocation?: (0 | 1)[][]
295+
): Point[] {
296+
if (start.x === end.x && start.y === end.y) return [];
297+
298+
let trimmedStart = start;
299+
let trimmedEnd = end;
300+
301+
if (!gridAllocation) {
302+
[trimmedStart, trimmedEnd] = trimStartEnd(start, end, gridSize);
303+
gridAllocation = emptyAllocation(trimmedStart, trimmedEnd, gridSize);
304+
}
305+
306+
const graph: GraphNode[] = createGraph(gridAllocation!, gridSize);
307+
308+
const shortesPath = getPath(graph, trimmedStart, trimmedEnd);
309+
310+
return fullPath(shortesPath, start, end, trimmedStart, trimmedEnd);
311+
}

0 commit comments

Comments
 (0)