From 5da476ee64168edfb811d2f5960cd6153cd68814 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 13 Feb 2024 10:57:03 +0100 Subject: [PATCH] fixed port order to group edges better --- .../stpa/diagram/diagram-generator.ts | 39 ++++++++++++----- .../stpa/diagram/layout-config.ts | 14 +++--- .../stpa/diagram/stpa-interfaces.ts | 3 +- .../stpa/diagram/stpa-model.ts | 2 +- .../src-language-server/stpa/diagram/utils.ts | 43 ++++++++++++++++--- .../src-language-server/stpa/stpa-module.ts | 2 +- extension/src-webview/di.config.ts | 6 +-- extension/src-webview/stpa/helper-methods.ts | 34 +++++++-------- extension/src-webview/stpa/stpa-model.ts | 5 ++- 9 files changed, 100 insertions(+), 48 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index ef820ca5..c86c7783 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -37,7 +37,7 @@ import { getDescription } from "../../utils"; import { StpaServices } from "../stpa-module"; import { collectElementsWithSubComps, leafElement } from "../utils"; import { filterModel } from "./filtering"; -import { CSEdge, CSNode, ParentNode, STPAEdge, STPANode, STPAPort } from "./stpa-interfaces"; +import { CSEdge, CSNode, ParentNode, STPAEdge, STPANode, PastaPort } from "./stpa-interfaces"; import { CS_EDGE_TYPE, CS_INTERMEDIATE_EDGE_TYPE, @@ -53,7 +53,7 @@ import { STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE, STPA_NODE_TYPE, - STPA_PORT_TYPE, + PORT_TYPE, } from "./stpa-model"; import { StpaSynthesisOptions, showLabelsValue } from "./stpa-synthesis-options"; import { @@ -64,6 +64,7 @@ import { getTargets, setLevelOfCSNodes, setLevelsForSTPANodes, + sortPorts, } from "./utils"; export class StpaDiagramGenerator extends LangiumDiagramGenerator { @@ -271,6 +272,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ...this.generateVerticalCSEdges(filteredModel.controlStructure.nodes, args), //...this.generateHorizontalCSEdges(filteredModel.controlStructure.edges, args) ]; + sortPorts(CSChildren.filter(node => node.type.startsWith("node")) as CSNode[]); // add control structure to roots children rootChildren.push({ type: PARENT_TYPE, @@ -864,15 +866,30 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { } protected generateIntermediateCSEdges( - source: AstNode | undefined, - target: AstNode | undefined, + source: Node | undefined, + target: Node | undefined, edgeId: string, edgeType: EdgeType, args: GeneratorContext, ancestor?: Node | Graph ): { sourcePort: string; targetPort: string } { - const sources = this.generatePortsForCSHierarchy(source, edgeId, edgeType === EdgeType.CONTROL_ACTION ? PortSide.SOUTH : PortSide.NORTH, args.idCache, ancestor); - const targets = this.generatePortsForCSHierarchy(target, edgeId, edgeType === EdgeType.CONTROL_ACTION ? PortSide.NORTH : PortSide.SOUTH, args.idCache, ancestor); + const assocEdge = { node1: source?.name ?? "", node2: target?.name ?? "" }; + const sources = this.generatePortsForCSHierarchy( + source, + assocEdge, + edgeId, + edgeType === EdgeType.CONTROL_ACTION ? PortSide.SOUTH : PortSide.NORTH, + args.idCache, + ancestor + ); + const targets = this.generatePortsForCSHierarchy( + target, + assocEdge, + edgeId, + edgeType === EdgeType.CONTROL_ACTION ? PortSide.NORTH : PortSide.SOUTH, + args.idCache, + ancestor + ); for (let i = 0; i < sources.nodes.length - 1; i++) { const sEdgeType = CS_INTERMEDIATE_EDGE_TYPE; sources.nodes[i + 1]?.children?.push( @@ -912,6 +929,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // adds ports for current node and its (grand)parents up to the ancestor. The ancestor get no port. protected generatePortsForCSHierarchy( current: AstNode | undefined, + assocEdge: { node1: string; node2: string }, edgeId: string, side: PortSide, idCache: IdCache, @@ -928,12 +946,12 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { if (invisibleChild && ids.length !== 0) { // add port for the invisible node first const invisiblePortId = idCache.uniqueId(edgeId + "_newTransition"); - invisibleChild.children?.push(this.createPort(invisiblePortId, side)); + invisibleChild.children?.push(this.createPort(invisiblePortId, side, assocEdge)); ids.push(invisiblePortId); nodes.push(invisibleChild); } const nodePortId = idCache.uniqueId(edgeId + "_newTransition"); - currentNode?.children?.push(this.createPort(nodePortId, side)); + currentNode?.children?.push(this.createPort(nodePortId, side, assocEdge)); ids.push(nodePortId); nodes.push(currentNode); current = current?.$container; @@ -982,11 +1000,12 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param side The side of the port. * @returns an STPAPort. */ - protected createPort(id: string, side: PortSide): STPAPort { + protected createPort(id: string, side: PortSide, assocEdge?: { node1: string; node2: string }): PastaPort { return { - type: STPA_PORT_TYPE, + type: PORT_TYPE, id: id, side: side, + assocEdge: assocEdge, }; } diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index dbbd80d8..7923f107 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -19,7 +19,7 @@ import { LayoutOptions } from "elkjs"; import { DefaultLayoutConfigurator } from "sprotty-elk/lib/elk-layout"; import { SGraph, SModelIndex, SNode, SPort } from "sprotty-protocol"; -import { CSNode, ParentNode, STPANode, STPAPort } from "./stpa-interfaces"; +import { CSNode, ParentNode, STPANode, PastaPort } from "./stpa-interfaces"; import { CS_NODE_TYPE, INVISIBLE_NODE_TYPE, @@ -27,7 +27,7 @@ import { PROCESS_MODEL_NODE_TYPE, PortSide, STPA_NODE_TYPE, - STPA_PORT_TYPE, + PORT_TYPE, } from "./stpa-model"; export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { @@ -95,10 +95,10 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { } processModelNodeOptions(snode: SNode): LayoutOptions | undefined { return { - "org.eclipse.elk.separateConnectedComponents": "false", - "org.eclipse.elk.direction": "DOWN", "org.eclipse.elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES", - "org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true" + "org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true", + // TODO: wait for node size fix in elkjs + // "org.eclipse.elk.algorithm": "rectpacking", }; } @@ -172,9 +172,9 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { } protected portOptions(sport: SPort, index: SModelIndex): LayoutOptions | undefined { - if (sport.type === STPA_PORT_TYPE) { + if (sport.type === PORT_TYPE) { let side = ""; - switch ((sport as STPAPort).side) { + switch ((sport as PastaPort).side) { case PortSide.WEST: side = "WEST"; break; diff --git a/extension/src-language-server/stpa/diagram/stpa-interfaces.ts b/extension/src-language-server/stpa/diagram/stpa-interfaces.ts index e2dba7ae..e3522a50 100644 --- a/extension/src-language-server/stpa/diagram/stpa-interfaces.ts +++ b/extension/src-language-server/stpa/diagram/stpa-interfaces.ts @@ -44,8 +44,9 @@ export interface STPANode extends SNode { } /** Port representing a port in the STPA graph. */ -export interface STPAPort extends SPort { +export interface PastaPort extends SPort { side?: PortSide + assocEdge?: {node1: string, node2: string} } /** diff --git a/extension/src-language-server/stpa/diagram/stpa-model.ts b/extension/src-language-server/stpa/diagram/stpa-model.ts index 95a8f9e8..f8d6ec42 100644 --- a/extension/src-language-server/stpa/diagram/stpa-model.ts +++ b/extension/src-language-server/stpa/diagram/stpa-model.ts @@ -27,7 +27,7 @@ export const CS_EDGE_TYPE = 'edge:controlStructure'; export const STPA_EDGE_TYPE = 'edge:stpa'; export const STPA_INTERMEDIATE_EDGE_TYPE = 'edge:stpa-intermediate'; export const CS_INTERMEDIATE_EDGE_TYPE = 'edge:cs-intermediate'; -export const STPA_PORT_TYPE = 'port:stpa'; +export const PORT_TYPE = 'port:pasta'; export const HEADER_LABEL_TYPE = 'label:header'; /** diff --git a/extension/src-language-server/stpa/diagram/utils.ts b/extension/src-language-server/stpa/diagram/utils.ts index 335d8303..571a43ed 100644 --- a/extension/src-language-server/stpa/diagram/utils.ts +++ b/extension/src-language-server/stpa/diagram/utils.ts @@ -34,9 +34,10 @@ import { isSystemResponsibilities, isUCA, } from "../../generated/ast"; -import { STPANode } from "./stpa-interfaces"; +import { CSNode, PastaPort, STPANode } from "./stpa-interfaces"; import { STPAAspect } from "./stpa-model"; import { groupValue } from "./stpa-synthesis-options"; +import { SModelElement } from "sprotty-protocol" /** * Getter for the references contained in {@code node}. @@ -364,8 +365,7 @@ export function getAspectsThatShouldHaveDesriptions(model: Model): STPAAspect[] return aspectsToShowDescriptions; } - -export function getCommonAncestor(node: Node, target: Node): Node|Graph | undefined { +export function getCommonAncestor(node: Node, target: Node): Node | Graph | undefined { const nodeAncestors = getAncestors(node); const targetAncestors = getAncestors(target); for (const ancestor of nodeAncestors) { @@ -376,12 +376,43 @@ export function getCommonAncestor(node: Node, target: Node): Node|Graph | undefi return undefined; } -export function getAncestors(node: Node): (Node|Graph)[] { - const ancestors: (Node|Graph)[] = []; +export function getAncestors(node: Node): (Node | Graph)[] { + const ancestors: (Node | Graph)[] = []; let current: Node | Graph | undefined = node; while (current?.$type !== "Graph") { ancestors.push(current.$container); current = current.$container; } return ancestors; -} \ No newline at end of file +} + +export function sortPorts(nodes: CSNode[]): void { + for (const node of nodes) { + const children = node.children?.filter(child => child.type.startsWith("node")) as CSNode[]; + sortPorts(children); + const ports: PastaPort[] = [] + const otherChildren: SModelElement[] = [] + node.children?.forEach(child => { + if (child.type.startsWith("port")) { + ports.push(child as any as PastaPort); + } else { + otherChildren.push(child); + } + }); + + const newPorts: PastaPort[] = []; + for (const port of ports) { + newPorts.push(port); + if (port.assocEdge) { + for (const otherPort of ports) { + if (port.assocEdge.node1 == otherPort.assocEdge?.node2 && port.assocEdge.node2 == otherPort.assocEdge.node1) { + newPorts.push(otherPort); + ports.splice(ports.indexOf(otherPort), 1); + } + } + } + } + + node.children = [...otherChildren, ...newPorts]; + } +} diff --git a/extension/src-language-server/stpa/stpa-module.ts b/extension/src-language-server/stpa/stpa-module.ts index e6581f8b..19d643d7 100644 --- a/extension/src-language-server/stpa/stpa-module.ts +++ b/extension/src-language-server/stpa/stpa-module.ts @@ -95,7 +95,7 @@ export const STPAModule: Module new StpaValidator(), }, layout: { - ElkFactory: () => () => new ElkConstructor({ algorithms: ["layered"] }), + ElkFactory: () => () => new ElkConstructor({ algorithms: ["layered", "rectpacking"] }), ElementFilter: () => new DefaultElementFilter(), LayoutConfigurator: () => new StpaLayoutConfigurator(), }, diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index 9f30f576..b397cbcf 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -62,11 +62,11 @@ import { PROCESS_MODEL_NODE_TYPE, STPAEdge, STPANode, - STPAPort, + PastaPort, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE, STPA_NODE_TYPE, - STPA_PORT_TYPE, + PORT_TYPE, } from "./stpa/stpa-model"; import { StpaMouseListener } from "./stpa/stpa-mouselistener"; import { @@ -118,7 +118,7 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = configureModelElement(context, STPA_INTERMEDIATE_EDGE_TYPE, STPAEdge, IntermediateEdgeView); configureModelElement(context, CS_INTERMEDIATE_EDGE_TYPE, CSEdge, IntermediateEdgeView); configureModelElement(context, CS_EDGE_TYPE, CSEdge, PolylineArrowEdgeView); - configureModelElement(context, STPA_PORT_TYPE, STPAPort, PortView); + configureModelElement(context, PORT_TYPE, PastaPort, PortView); // FTA configureModelElement(context, FTA_EDGE_TYPE, FTAEdge, PolylineArrowEdgeViewFTA); diff --git a/extension/src-webview/stpa/helper-methods.ts b/extension/src-webview/stpa/helper-methods.ts index 5af3b77a..dfae3824 100644 --- a/extension/src-webview/stpa/helper-methods.ts +++ b/extension/src-webview/stpa/helper-methods.ts @@ -16,7 +16,7 @@ */ import { SEdge, SModelElement, SNode } from "sprotty"; -import { PortSide, STPAAspect, STPAEdge, STPANode, STPAPort, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE, STPA_NODE_TYPE, STPA_PORT_TYPE } from "./stpa-model"; +import { PortSide, STPAAspect, STPAEdge, STPANode, PastaPort, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE, STPA_NODE_TYPE, PORT_TYPE } from "./stpa-model"; /** * Collects all children of the nodes in {@code nodes}. @@ -45,23 +45,23 @@ export function flagConnectedElements(node: SNode): (STPANode | STPAEdge)[] { // flagging four sub components flaggingOutgoingForSubcomponents(node as STPANode, elements); // to find the connected edges and nodes of the selected node, the ports are inspected - for (const port of node.children.filter(child => child.type === STPA_PORT_TYPE)) { + for (const port of node.children.filter(child => child.type === PORT_TYPE)) { // the edges for a port are defined in the parent node // hence we have to search in the children of the parent node for (const child of node.parent.children) { if ((node as STPANode).aspect === STPAAspect.SYSTEMCONSTRAINT && (node.parent as STPANode).aspect !== STPAAspect.SYSTEMCONSTRAINT) { // for the top system constraint node the intermediate outoging edges should not be highlighted if (child.type === STPA_EDGE_TYPE) { - flagIncomingEdges(child as STPAEdge, port as STPAPort, elements); - flagOutgoingEdges(child as STPAEdge, port as STPAPort, elements); + flagIncomingEdges(child as STPAEdge, port as PastaPort, elements); + flagOutgoingEdges(child as STPAEdge, port as PastaPort, elements); } } else if (child.type === STPA_INTERMEDIATE_EDGE_TYPE) { // the intermediate edges should in general only be highlighted when they are outgoing edges - flagOutgoingEdges(child as STPAEdge, port as STPAPort, elements); + flagOutgoingEdges(child as STPAEdge, port as PastaPort, elements); } else if (child.type === STPA_EDGE_TYPE) { // flag incoming and outgoing edges - flagIncomingEdges(child as STPAEdge, port as STPAPort, elements); - flagOutgoingEdges(child as STPAEdge, port as STPAPort, elements); + flagIncomingEdges(child as STPAEdge, port as PastaPort, elements); + flagOutgoingEdges(child as STPAEdge, port as PastaPort, elements); } } } @@ -85,20 +85,20 @@ function flagPredNodes(edge: SEdge, elements: SModelElement[]): void { for (const subH of subHazards) { subH.highlight = true; elements.push(subH); - for (const port of subH.children.filter(child => child.type === STPA_PORT_TYPE && (child as STPAPort).side === PortSide.SOUTH)) { + for (const port of subH.children.filter(child => child.type === PORT_TYPE && (child as PastaPort).side === PortSide.SOUTH)) { for (const child of subH.parent.children) { if (child.type.startsWith('edge:stpa')) { - flagIncomingEdges(child as STPAEdge, port as STPAPort, elements); + flagIncomingEdges(child as STPAEdge, port as PastaPort, elements); } } } } } // flag incoming edges from node by going over the ports - for (const port of node.children.filter(child => child.type === STPA_PORT_TYPE && (child as STPAPort).side === PortSide.SOUTH)) { + for (const port of node.children.filter(child => child.type === PORT_TYPE && (child as PastaPort).side === PortSide.SOUTH)) { for (const child of node.parent.children) { if (child.type.startsWith('edge:stpa')) { - flagIncomingEdges(child as STPAEdge, port as STPAPort, elements); + flagIncomingEdges(child as STPAEdge, port as PastaPort, elements); } } } @@ -114,10 +114,10 @@ function flagSuccNodes(edge: SEdge, elements: SModelElement[]): void { elements.push(node); flaggingOutgoingForSubcomponents(node, elements); // flag outgoing edges from node by going over the ports - for (const port of node.children.filter(child => child.type === STPA_PORT_TYPE && (child as STPAPort).side === PortSide.NORTH)) { + for (const port of node.children.filter(child => child.type === PORT_TYPE && (child as PastaPort).side === PortSide.NORTH)) { for (const child of node.parent.children) { if (child.type.startsWith('edge:stpa')) { - flagOutgoingEdges(child as STPAEdge, port as STPAPort, elements); + flagOutgoingEdges(child as STPAEdge, port as PastaPort, elements); } } } @@ -137,10 +137,10 @@ function flaggingOutgoingForSubcomponents(node: STPANode, elements: SModelElemen if (isSubHazard(node)) { (node.parent as STPANode).highlight = true; elements.push(node.parent as STPANode); - for (const port of (node.parent as STPANode).children.filter(child => child.type === STPA_PORT_TYPE && (child as STPAPort).side === PortSide.NORTH)) { + for (const port of (node.parent as STPANode).children.filter(child => child.type === PORT_TYPE && (child as PastaPort).side === PortSide.NORTH)) { for (const child of (node.parent as STPANode).parent.children) { if (child.type.startsWith('edge:stpa')) { - flagOutgoingEdges(child as STPAEdge, port as STPAPort, elements); + flagOutgoingEdges(child as STPAEdge, port as PastaPort, elements); } } } @@ -153,7 +153,7 @@ function flaggingOutgoingForSubcomponents(node: STPANode, elements: SModelElemen * @param port The port which is checked to be the source of the {@code edge}. * @param elements The elements which should be highlighted. */ -function flagIncomingEdges(edge: STPAEdge, port: STPAPort, elements: SModelElement[]): void { +function flagIncomingEdges(edge: STPAEdge, port: PastaPort, elements: SModelElement[]): void { if (edge.targetId === port.id) { // if the edge leads to another edge, highlight all connected edges let furtherEdge: STPAEdge | undefined = edge; @@ -174,7 +174,7 @@ function flagIncomingEdges(edge: STPAEdge, port: STPAPort, elements: SModelEleme * @param port The port which is checked to be the source of the {@code edge}. * @param elements The elements which should be highlighted. */ -function flagOutgoingEdges(edge: STPAEdge, port: STPAPort, elements: SModelElement[]): void { +function flagOutgoingEdges(edge: STPAEdge, port: PastaPort, elements: SModelElement[]): void { if (edge.sourceId === port.id) { // if the edge leads to another edge, highlight all connected edges let furtherEdge: STPAEdge | undefined = edge; diff --git a/extension/src-webview/stpa/stpa-model.ts b/extension/src-webview/stpa/stpa-model.ts index e3c20551..d1cb02d4 100644 --- a/extension/src-webview/stpa/stpa-model.ts +++ b/extension/src-webview/stpa/stpa-model.ts @@ -29,7 +29,7 @@ export const CS_EDGE_TYPE = 'edge:controlStructure'; export const STPA_EDGE_TYPE = 'edge:stpa'; export const STPA_INTERMEDIATE_EDGE_TYPE = 'edge:stpa-intermediate'; export const CS_INTERMEDIATE_EDGE_TYPE = 'edge:cs-intermediate'; -export const STPA_PORT_TYPE = 'port:stpa'; +export const PORT_TYPE = 'port:pasta'; export const HEADER_LABEL_TYPE = 'label:header'; export class ParentNode extends SNode { @@ -62,8 +62,9 @@ export class STPAEdge extends SEdge { } /** Port representing a port in the STPA graph. */ -export class STPAPort extends SPort { +export class PastaPort extends SPort { side?: PortSide; + assocEdge?: {node1: string, node2: string} } /**