Skip to content

Commit 36d5a77

Browse files
authored
Export Layout & handle service like a service (#70)
1 parent 7867c36 commit 36d5a77

9 files changed

Lines changed: 120 additions & 144 deletions

File tree

projects/frank-config-layout/src/lib/dimensions.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
1-
import { Dimensions } from '../public_api';
21
import { EdgeLabelDimensions } from './graphics/edge-label-layouter';
32
import { NodeAndEdgeDimensions } from './graphics/layout';
43
import { SvgGenerationDimensions } from './graphics/svg-generator';
54
import { calculateAverageFontCharacterWidth, NodeTextDimensions } from './model/text';
65

6+
export interface Dimensions {
7+
nodeTextFontSize: number; // 16
8+
nodeTextBorder: number;
9+
horizontalNodeBorder: number;
10+
intermediateWidth: number;
11+
layerHeight: number;
12+
layerDistance: number;
13+
nodeBoxHeight: number;
14+
boxConnectorAreaPerc: number;
15+
intermediateLayerPassedByVerticalLine: boolean;
16+
boxCrossProtectionMargin: number;
17+
lineTransgressionPerc: number;
18+
edgeLabelFontSize: number;
19+
preferredVertDistanceFromOrigin: number;
20+
strictlyKeepLabelOutOfBox: boolean;
21+
}
22+
723
export interface DerivedDimensions
824
extends EdgeLabelDimensions, NodeAndEdgeDimensions, SvgGenerationDimensions, NodeTextDimensions {}
925

projects/frank-config-layout/src/lib/flow2svg.spec.ts renamed to projects/frank-config-layout/src/lib/flow-layout.spec.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Flow2svgService } from './flow2svg';
2-
import { getFactoryDimensions, SvgResult } from '../public_api';
1+
import { FlowLayoutService, LayoutStatisticsResult } from './flow-layout';
2+
import { getFactoryDimensions } from '../public_api';
33

44
const input = `Start('<text>My start</text>'):::normal
55
N1('<text>Node 1</text>'):::normal
@@ -282,11 +282,11 @@ const expectedMultiline = `<svg class="svg" xmlns="http://www.w3.org/2000/svg"
282282
<g text-anchor="middle" dominant-baseline="middle"><text class="label-text" x="92" y="78" font-size="10">success</text><text class="label-text" x="92" y="94" font-size="10">other</text><text class="label-text" x="138" y="78" font-size="10">success</text><text class="label-text" x="138" y="94" font-size="10">other</text><text class="label-text" x="207" y="86" font-size="10">success</text><text class="label-text" x="101" y="206" font-size="10">success</text><text class="label-text" x="160" y="206" font-size="10">failure</text><text class="label-text" x="249" y="206" font-size="10">success</text><text class="label-text" x="93" y="326" font-size="10">success</text></g></svg>`;
283283

284284
describe('Flow2svg - please maintain this test using the GUI', () => {
285-
let service: Flow2svgService;
285+
let service: FlowLayoutService;
286286

287287
beforeEach(() => {
288288
// No need to test injection - if injection does not work then the app does not show
289-
service = new Flow2svgService(getFactoryDimensions());
289+
service = new FlowLayoutService(getFactoryDimensions());
290290
});
291291

292292
it('Test the plain SVG', (done) => {
@@ -305,18 +305,18 @@ describe('Flow2svg - please maintain this test using the GUI', () => {
305305
});
306306
});
307307

308-
it('Test with statistics', (done) => {
309-
service.flow2svgStatistics(input).then((statistics) => {
310-
expect(statistics.svg).toEqual(expectedSvg);
311-
expect(statistics.numNodes).toEqual(5);
312-
expect(statistics.numEdges).toEqual(7);
313-
expect(statistics.numNodeVisitsDuringLayerCalculation).toEqual(8);
314-
done();
315-
});
308+
it('Test with statistics', async () => {
309+
const flowSvg = await service.flow2svg(input);
310+
expect(flowSvg).toEqual(expectedSvg);
311+
312+
const statistics = await service.flow2LayoutStatistics(input);
313+
expect(statistics.numNodes).toEqual(5);
314+
expect(statistics.numEdges).toEqual(7);
315+
expect(statistics.numNodeVisitsDuringLayerCalculation).toEqual(8);
316316
});
317317

318318
it('Test that real calculation is done only once', (done) => {
319-
const first: Promise<SvgResult> = service.flow2svgStatistics(input);
319+
const first: Promise<LayoutStatisticsResult> = service.flow2LayoutStatistics(input);
320320
const second: Promise<string> = service.flow2svg(input);
321321
Promise.all([first, second]).then(() => {
322322
expect(service.numSvgCalculations).toEqual(1);

projects/frank-config-layout/src/lib/flow2svg.ts renamed to projects/frank-config-layout/src/lib/flow-layout.ts

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -26,72 +26,78 @@ import {
2626
calculateLayerNumbersLongestPath,
2727
IntermediatesCreationResult,
2828
} from './model/horizontal-grouping';
29-
import { SvgResult, Dimensions } from '../public_api';
3029
import { LayoutModel, LayoutModelBuilder } from './model/layout-model';
31-
import { DerivedDimensions, getDerivedDimensions } from './dimensions';
30+
import { DerivedDimensions, Dimensions, getDerivedDimensions } from './dimensions';
3231

33-
export class Flow2svgService {
34-
private cache = new AsynchronousCache<SvgResult>();
32+
export interface LayoutStatisticsResult {
33+
layout: Layout;
34+
numNodes: number;
35+
numEdges: number;
36+
numNodeVisitsDuringLayerCalculation: number;
37+
}
38+
39+
export class FlowLayoutService {
40+
private cache = new AsynchronousCache<LayoutStatisticsResult>();
3541

3642
private _numSvgCalculations = 0;
3743
get numSvgCalculations(): number {
3844
return this._numSvgCalculations;
3945
}
4046

4147
getHashes(): string[] {
42-
return [...this.cache.getSortedKeys()];
48+
return this.cache.getSortedKeys();
4349
}
4450

45-
private dimensions: DerivedDimensions;
51+
private readonly dimensions: DerivedDimensions;
4652

4753
constructor(dimensions: Dimensions) {
4854
this.dimensions = getDerivedDimensions(dimensions);
4955
}
5056

5157
async flow2svg(flow: string): Promise<string> {
52-
const statistics = await this.flow2svgStatistics(flow);
53-
return statistics.svg;
58+
const statistics = await this.flow2LayoutStatistics(flow);
59+
return generateSvg(statistics.layout, this.dimensions);
60+
}
61+
62+
async flow2Layout(flow: string): Promise<Layout> {
63+
const statistics = await this.flow2LayoutStatistics(flow);
64+
return statistics.layout;
5465
}
5566

56-
async flow2svgStatistics(flow: string): Promise<SvgResult> {
57-
let hash: string;
67+
async flow2LayoutStatistics(flow: string): Promise<LayoutStatisticsResult> {
5868
try {
59-
hash = await sha256(flow);
69+
const hash = await sha256(flow);
70+
return this.cache.get(hash, () => this.flow2LayoutStatisticsImpl(flow));
6071
} catch (error) {
61-
console.log(error);
62-
throw new Error('Could not calculate hash');
72+
throw new Error('Could not calculate hash', error as Error);
6373
}
64-
return await this.cache.get(hash, () => this.flow2svgStatisticsImpl(flow));
6574
}
6675

67-
private async flow2svgStatisticsImpl(flow: string): Promise<SvgResult> {
76+
private async flow2LayoutStatisticsImpl(flow: string): Promise<LayoutStatisticsResult> {
6877
++this._numSvgCalculations;
69-
const b: FlowGraph = getGraphFromFlow(flow, this.dimensions);
70-
const g: OriginalGraph = findErrorFlow(b);
78+
const flowGraph: FlowGraph = getGraphFromFlow(flow, this.dimensions);
79+
const graph: OriginalGraph = findErrorFlow(flowGraph);
7180
let numNodeVisits = 0;
72-
const nodeIdToLayer: Map<string, number> = calculateLayerNumbersLongestPath(g, () => ++numNodeVisits);
73-
const intermediates: IntermediatesCreationResult = introduceIntermediateNodesAndEdges(g, nodeIdToLayer);
74-
let lb: LayoutBase;
75-
try {
76-
lb = LayoutBase.create(
77-
intermediates.intermediate.nodes.map((n) => n.id),
78-
intermediates.intermediate,
79-
);
80-
} catch (error) {
81-
throw error;
82-
}
83-
lb = minimizeNumCrossings(lb);
84-
const layoutModel: LayoutModel = new LayoutModelBuilder(lb, intermediates.intermediate).run();
81+
const nodeIdToLayer: Map<string, number> = calculateLayerNumbersLongestPath(graph, () => ++numNodeVisits);
82+
const intermediates: IntermediatesCreationResult = introduceIntermediateNodesAndEdges(graph, nodeIdToLayer);
83+
const layoutBase = LayoutBase.create(
84+
intermediates.intermediate.nodes.map((n) => n.id),
85+
intermediates.intermediate,
86+
);
87+
const layoutModel: LayoutModel = new LayoutModelBuilder(
88+
minimizeNumCrossings(layoutBase),
89+
intermediates.intermediate,
90+
).run();
8591
const layout: Layout = new LayoutBuilder(
8692
layoutModel,
8793
intermediates.original,
8894
this.dimensions,
8995
this.dimensions,
9096
).run();
9197
return {
92-
svg: generateSvg(layout, this.dimensions),
93-
numNodes: g.nodes.length,
94-
numEdges: g.edges.length,
98+
layout,
99+
numNodes: graph.nodes.length,
100+
numEdges: graph.edges.length,
95101
numNodeVisitsDuringLayerCalculation: numNodeVisits,
96102
};
97103
}

projects/frank-config-layout/src/lib/graphics/svg-generator.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,7 @@ function getNodeGroupClass(id: string): string {
120120
}
121121

122122
function getRectangleClass(n: PlacedNode): string {
123-
if (n.errorStatus === ERROR_STATUS_ERROR) {
124-
return 'rectangle errorOutline';
125-
} else {
126-
return 'rectangle';
127-
}
123+
return n.errorStatus === ERROR_STATUS_ERROR ? 'rectangle errorOutline' : 'rectangle';
128124
}
129125

130126
function renderEdges(edges: LayoutLineSegment[]): string {
@@ -143,11 +139,7 @@ function getEdgeGroupClass(key: string): string {
143139
}
144140

145141
function getMarkerEnd(lineSegment: LayoutLineSegment): string {
146-
if (lineSegment.isLastLineSegment) {
147-
return 'marker-end="url(#arrow)"';
148-
} else {
149-
return '';
150-
}
142+
return lineSegment.isLastLineSegment ? 'marker-end="url(#arrow)"' : '';
151143
}
152144

153145
function classOfLine(edge: LayoutLineSegment): string {
@@ -172,9 +164,9 @@ function renderLabel(label: EdgeLabel, edgeLabelFontSize: number, estEdgeLabelCh
172164
commonItemHeight: edgeLabelFontSize,
173165
});
174166
let result: string = '';
175-
for (let i = 0; i < label.text.lines.length; ++i) {
176-
const p: Point = coordinates[i];
177-
result += renderSingleLayerText(p.x, p.y, edgeLabelFontSize, label.text.lines[i].svg);
167+
for (let index = 0; index < label.text.lines.length; ++index) {
168+
const p: Point = coordinates[index];
169+
result += renderSingleLayerText(p.x, p.y, edgeLabelFontSize, label.text.lines[index].svg);
178170
}
179171
return result;
180172
}
@@ -198,9 +190,9 @@ function getSvgTextElements(node: PlacedNode, border: number, fontSize: number):
198190
itemWidths: nodeText.parts.map((p) => p.innerWidth),
199191
});
200192
let totalSvgText = '';
201-
for (let i = 0; i < nodeText.parts.length; ++i) {
202-
const p: Point = textCoordinates[i];
203-
totalSvgText += getSvgTextElement(nodeText.parts[i], p.x, p.y);
193+
for (let index = 0; index < nodeText.parts.length; ++index) {
194+
const p: Point = textCoordinates[index];
195+
totalSvgText += getSvgTextElement(nodeText.parts[index], p.x, p.y);
204196
}
205197
return totalSvgText;
206198
}

projects/frank-config-layout/src/lib/model/layout-base.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2025 WeAreFrank!
2+
Copyright 2025, 2026 WeAreFrank!
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -219,14 +219,18 @@ export class NumCrossingsJudgement {
219219
}
220220
}
221221

222-
export function minimizeNumCrossings(lb: LayoutBase): LayoutBase {
223-
const current = lb.clone();
222+
export function minimizeNumCrossings(layoutBase: LayoutBase): LayoutBase {
223+
const current = layoutBase.clone();
224224
while (true) {
225225
const crossingsChanges = calculateNumCrossingsChangesFromAligning(current);
226226
const judgements: NumCrossingsJudgement[] = [];
227-
for (let layerNumber = 0; layerNumber < lb.numLayers; ++layerNumber) {
227+
for (let layerNumber = 0; layerNumber < layoutBase.numLayers; ++layerNumber) {
228228
judgements.push(
229-
new NumCrossingsJudgement(layerNumber, lb.getIdsOfLayer(layerNumber).length, -crossingsChanges[layerNumber]),
229+
new NumCrossingsJudgement(
230+
layerNumber,
231+
layoutBase.getIdsOfLayer(layerNumber).length,
232+
-crossingsChanges[layerNumber],
233+
),
230234
);
231235
}
232236
judgements.sort((a, b) => a.compareTo(b));

projects/frank-config-layout/src/lib/model/layout-model.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2025 WeAreFrank!
2+
Copyright 2025, 2026 WeAreFrank!
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -67,10 +67,10 @@ export class LayoutModelBuilder<T extends WithLayerNumber, C extends Connection<
6767
private connectionsByEdgeKey = new Map<string, LayoutConnection>();
6868

6969
constructor(
70-
readonly lb: LayoutBase,
71-
readonly g: Graph<T, C>,
70+
readonly layoutBase: LayoutBase,
71+
readonly graph: Graph<T, C>,
7272
) {
73-
for (let layer = 0; layer < lb.numLayers; ++layer) {
73+
for (let layer = 0; layer < layoutBase.numLayers; ++layer) {
7474
this.positionsOfLayer.push([]);
7575
}
7676
}
@@ -81,7 +81,7 @@ export class LayoutModelBuilder<T extends WithLayerNumber, C extends Connection<
8181
this.buildConnectors();
8282
this.buildConnectionsFromEdges();
8383
return new LayoutModel(
84-
this.lb.numLayers,
84+
this.layoutBase.numLayers,
8585
this.positionsByKey,
8686
this.connectorsByKey,
8787
this.positionsOfLayer,
@@ -93,8 +93,8 @@ export class LayoutModelBuilder<T extends WithLayerNumber, C extends Connection<
9393
}
9494

9595
private establishPositions(): void {
96-
for (let layerNumber = 0; layerNumber < this.lb.numLayers; ++layerNumber) {
97-
for (const [position, id] of this.lb.getIdsOfLayer(layerNumber).entries()) {
96+
for (let layerNumber = 0; layerNumber < this.layoutBase.numLayers; ++layerNumber) {
97+
for (const [position, id] of this.layoutBase.getIdsOfLayer(layerNumber).entries()) {
9898
const positionObject: LayoutPosition = new LayoutPosition(layerNumber, position, id);
9999
this.positionsByKey.set(positionObject.key, positionObject);
100100
this.positionsOfLayer[layerNumber].push(positionObject);
@@ -105,7 +105,7 @@ export class LayoutModelBuilder<T extends WithLayerNumber, C extends Connection<
105105

106106
private relatePositions(): void {
107107
this.forEachPositionAndAdjacentLayerCombi((referencePositionObject, otherLayerNumber) => {
108-
const positionsInOther: number[] = this.lb.getConnections(referencePositionObject.id, otherLayerNumber);
108+
const positionsInOther: number[] = this.layoutBase.getConnections(referencePositionObject.id, otherLayerNumber);
109109
for (const positionInOther of positionsInOther) {
110110
const relatedPositionObjectKey = `${otherLayerNumber}-${positionInOther}`;
111111
const relatedPositionObject: LayoutPosition | undefined = this.positionsByKey.get(relatedPositionObjectKey);
@@ -131,9 +131,9 @@ export class LayoutModelBuilder<T extends WithLayerNumber, C extends Connection<
131131
private forEachPositionAndAdjacentLayerCombi(
132132
action: (referencePositionObject: LayoutPosition, otherLayerNumber: number) => void,
133133
): void {
134-
for (let layerNumber = 0; layerNumber < this.lb.numLayers; ++layerNumber) {
134+
for (let layerNumber = 0; layerNumber < this.layoutBase.numLayers; ++layerNumber) {
135135
const otherLayerNumbers: number[] = [layerNumber - 1, layerNumber + 1].filter(
136-
(otherLayerNumber) => otherLayerNumber >= 0 && otherLayerNumber < this.lb.numLayers,
136+
(otherLayerNumber) => otherLayerNumber >= 0 && otherLayerNumber < this.layoutBase.numLayers,
137137
);
138138
for (const positionObject of this.positionsOfLayer[layerNumber]) {
139139
for (const otherLayerNumber of otherLayerNumbers) {
@@ -149,7 +149,7 @@ export class LayoutModelBuilder<T extends WithLayerNumber, C extends Connection<
149149
for (const relatedPositionObject of this.getRelatedPositionObjects(referencePositionObject, otherLayerNumber)) {
150150
const idRef = referencePositionObject.id;
151151
const idRel = relatedPositionObject.id;
152-
const edgesInOut: C[] = [this.g.searchEdge(idRel, idRef), this.g.searchEdge(idRef, idRel)].filter(
152+
const edgesInOut: C[] = [this.graph.searchEdge(idRel, idRef), this.graph.searchEdge(idRef, idRel)].filter(
153153
(edge) => edge !== undefined,
154154
);
155155
let edges: C[];

0 commit comments

Comments
 (0)