From 5a4341eb25a86302a69a4f85e5e4f5c444b9f1bb Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 31 Jan 2024 17:40:37 +0100 Subject: [PATCH 01/16] option to show process model of controller process model variable name is hown in bold and underneath the possible values --- .../stpa/diagram/diagram-generator.ts | 38 +++++++++++++++++-- .../stpa/diagram/layout-config.ts | 13 ++++++- .../stpa/diagram/stpa-model.ts | 1 + .../stpa/diagram/stpa-synthesis-options.ts | 26 +++++++++++++ extension/src-webview/css/diagram.css | 5 +++ extension/src-webview/di.config.ts | 3 ++ extension/src-webview/stpa/stpa-model.ts | 1 + extension/src-webview/stpa/stpa-views.tsx | 13 ++++++- 8 files changed, 92 insertions(+), 8 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 726d624b..9c5591b8 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -25,6 +25,7 @@ import { Model, Node, SystemConstraint, + Variable, VerticalEdge, isContext, isHazard, @@ -41,6 +42,7 @@ import { CS_NODE_TYPE, DUMMY_NODE_TYPE, EdgeType, + HEADER_LABEL_TYPE, PARENT_TYPE, PortSide, STPAAspect, @@ -296,11 +298,15 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { protected createControlStructureNode(node: Node, { idCache }: GeneratorContext): CSNode { const label = node.label ? node.label : node.name; const nodeId = idCache.uniqueId(node.name, node); + const children: SModelElement[] = this.createLabel([label], nodeId, idCache); + if (this.options.getShowProcessModels()) { + children.push(...this.createCSChildren(node.variables, idCache)); + } const csNode = { type: CS_NODE_TYPE, id: nodeId, level: node.level, - children: this.createLabel([label], nodeId, idCache), + children: children, layout: "stack", layoutOptions: { paddingTop: 10.0, @@ -313,6 +319,30 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { return csNode; } + protected createCSChildren(variables: Variable[], idCache: IdCache): SModelElement[] { + const csChildren: SModelElement[] = []; + for (const variable of variables) { + const label = variable.name; + const nodeId = idCache.uniqueId(variable.name, variable); + const values = variable.values?.map((value) => value.name); + const children = [...this.createLabel([label], nodeId, idCache, HEADER_LABEL_TYPE), ...this.createLabel(values, nodeId, idCache)]; + const csNode = { + type: CS_NODE_TYPE, + id: nodeId, + children: children, + layout: "stack", + layoutOptions: { + paddingTop: 10.0, + paddingBottom: 10.0, + paddingLeft: 10.0, + paddingRight: 10.0, + }, + } as CSNode; + csChildren.push(csNode); + } + return csChildren; + } + /** * Creates the edges for the control structure. * @param nodes The nodes of the control structure. @@ -890,12 +920,12 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param id The ID of the element for which the label should be generated. * @returns SLabel elements representing {@code label}. */ - protected createLabel(label: string[], id: string, idCache: IdCache): SLabel[] { + protected createLabel(label: string[], id: string, idCache: IdCache, type: string = "label:xref"): SLabel[] { const children: SLabel[] = []; if (label.find((l) => l !== "")) { label.forEach((l) => { children.push({ - type: "label:xref", + type: type, id: idCache.uniqueId(id + "_label"), text: l, } as SLabel); @@ -903,7 +933,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { } else { // needed for correct layout children.push({ - type: "label:xref", + type: type, id: idCache.uniqueId(id + "_label"), text: " ", } as SLabel); diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index 7717d970..0dfde42c 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -122,14 +122,23 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { } protected csNodeOptions(node: CSNode): LayoutOptions { - return { - 'org.eclipse.elk.nodeLabels.placement': "INSIDE V_CENTER H_CENTER", + const options: LayoutOptions = { 'org.eclipse.elk.partitioning.partition': "" + node.level, 'org.eclipse.elk.nodeSize.constraints': 'NODE_LABELS', // edges do no start at the border of the node 'org.eclipse.elk.spacing.portsSurrounding': '[top=10.0,left=10.0,bottom=10.0,right=10.0]', 'org.eclipse.elk.portConstraints': 'FIXED_SIDE', }; + if (node.children?.find(child => child.type.startsWith('node'))) { + options['org.eclipse.elk.nodeLabels.placement'] = "INSIDE V_TOP H_CENTER"; + // TODO: want to use rectpacking instead of layered but this does not work at the moment (sprotty issue) + options['org.eclipse.elk.direction'] = 'DOWN'; + options['org.eclipse.elk.separateConnectedComponents'] = 'false'; + } else { + // TODO: want H_LEFT but this expands the node more than needed + options['org.eclipse.elk.nodeLabels.placement'] = "INSIDE V_CENTER H_CENTER"; + } + return options; } protected portOptions(sport: SPort, index: SModelIndex): LayoutOptions | undefined { diff --git a/extension/src-language-server/stpa/diagram/stpa-model.ts b/extension/src-language-server/stpa/diagram/stpa-model.ts index ae461b04..9ad1525d 100644 --- a/extension/src-language-server/stpa/diagram/stpa-model.ts +++ b/extension/src-language-server/stpa/diagram/stpa-model.ts @@ -25,6 +25,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 STPA_PORT_TYPE = 'port:stpa'; +export const HEADER_LABEL_TYPE = 'label:header'; /** * The different aspects of STPA. diff --git a/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts b/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts index 20002df7..129b4fd9 100644 --- a/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts +++ b/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts @@ -40,6 +40,7 @@ const showLabelsID = "showLabels"; const filterCategoryID = "filterCategory"; const showControlStructureID = "showControlStructure"; +const showProcessModelsID = "showProcessModels"; const showRelationshipGraphID = "showRelationshipGraph"; /** @@ -78,6 +79,22 @@ const showControlStructureOption: ValuedSynthesisOption = { currentValue: true, }; +/** + * Boolean option to toggle the visualization of the control structure. + */ +const showProcessModelsOption: ValuedSynthesisOption = { + synthesisOption: { + id: showProcessModelsID, + name: "Show Process Models", + type: TransformationOptionType.CHECK, + initialValue: false, + currentValue: false, + values: [true, false], + category: filterCategory, + }, + currentValue: false, +}; + /** * Boolean option to toggle the visualization of the relationship graph. */ @@ -329,6 +346,7 @@ export class StpaSynthesisOptions extends SynthesisOptions { hideSafetyConstraintsOption, showLabelsOption, showControlStructureOption, + showProcessModelsOption, showRelationshipGraphOption, ] ); @@ -377,6 +395,14 @@ export class StpaSynthesisOptions extends SynthesisOptions { return this.getOption(showControlStructureID)?.currentValue; } + setShowProcessModels(value: boolean): void { + this.setOption(showProcessModelsID, value); + } + + getShowProcessModels(): boolean { + return this.getOption(showProcessModelsID)?.currentValue; + } + setHierarchy(value: boolean): void { this.setOption(hierarchyID, value); } diff --git a/extension/src-webview/css/diagram.css b/extension/src-webview/css/diagram.css index d1b36d33..afdfae53 100644 --- a/extension/src-webview/css/diagram.css +++ b/extension/src-webview/css/diagram.css @@ -5,6 +5,11 @@ @import "./fta-diagram.css"; @import "./context-menu.css"; +.header { + text-decoration-line: underline; + font-weight: bold; +} + /* sprotty and black/white colors */ .vscode-high-contrast .print-node { fill: black; diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index eff02c2d..a7d4b06f 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -55,6 +55,7 @@ import { CS_EDGE_TYPE, CS_NODE_TYPE, DUMMY_NODE_TYPE, + HEADER_LABEL_TYPE, PARENT_TYPE, STPAEdge, STPANode, @@ -67,6 +68,7 @@ import { import { StpaMouseListener } from "./stpa/stpa-mouselistener"; import { CSNodeView, + HeaderLabelView, IntermediateEdgeView, PolylineArrowEdgeView, PortView, @@ -96,6 +98,7 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = const context = { bind, unbind, isBound, rebind }; configureModelElement(context, "label", SLabel, SLabelView); configureModelElement(context, "label:xref", SLabel, SLabelView); + configureModelElement(context, HEADER_LABEL_TYPE, SLabel, HeaderLabelView); configureModelElement(context, "html", HtmlRoot, HtmlRootView); configureModelElement(context, "pre-rendered", PreRenderedElement, PreRenderedView); diff --git a/extension/src-webview/stpa/stpa-model.ts b/extension/src-webview/stpa/stpa-model.ts index 1ef0c53b..647e9d2f 100644 --- a/extension/src-webview/stpa/stpa-model.ts +++ b/extension/src-webview/stpa/stpa-model.ts @@ -27,6 +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 STPA_PORT_TYPE = 'port:stpa'; +export const HEADER_LABEL_TYPE = 'label:header'; export class ParentNode extends SNode { modelOrder: boolean; diff --git a/extension/src-webview/stpa/stpa-views.tsx b/extension/src-webview/stpa/stpa-views.tsx index 4b70a21a..7dd33bc7 100644 --- a/extension/src-webview/stpa/stpa-views.tsx +++ b/extension/src-webview/stpa/stpa-views.tsx @@ -18,10 +18,10 @@ /** @jsx svg */ import { inject, injectable } from 'inversify'; import { VNode } from 'snabbdom'; -import { IView, IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, SNode, SPort, svg, toDegrees } from 'sprotty'; +import { IView, IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, SLabel, SLabelView, SNode, SPort, svg, toDegrees } from 'sprotty'; import { DISymbol } from '../di.symbols'; import { ColorStyleOption, DifferentFormsOption, RenderOptionsRegistry } from '../options/render-options-registry'; -import { renderOval, renderDiamond, renderHexagon, renderMirroredTriangle, renderPentagon, renderRectangle, renderRoundedRectangle, renderTrapez, renderTriangle } from '../views-rendering'; +import { renderDiamond, renderHexagon, renderMirroredTriangle, renderOval, renderPentagon, renderRectangle, renderRoundedRectangle, renderTrapez, renderTriangle } from '../views-rendering'; import { collectAllChildren } from './helper-methods'; import { CSEdge, CS_EDGE_TYPE, EdgeType, STPAAspect, STPAEdge, STPANode, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE } from './stpa-model'; @@ -238,4 +238,13 @@ export class PortView implements IView { render(model: SPort, context: RenderingContext): VNode { return ; } +} + +@injectable() +export class HeaderLabelView extends SLabelView { + render(label: Readonly, context: RenderingContext): VNode | undefined { + return + {super.render(label, context)} + + } } \ No newline at end of file From 964a96868b65ef715a13ef8705512ffdb8fbbb47 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 7 Feb 2024 10:35:15 +0100 Subject: [PATCH 02/16] control structure nodes can have children --- .../stpa/diagram/diagram-generator.ts | 83 +++++++---- .../stpa/diagram/layout-config.ts | 130 ++++++++++-------- .../stpa/diagram/stpa-model.ts | 1 + .../src-language-server/stpa/diagram/utils.ts | 13 +- .../src-language-server/stpa/stpa.langium | 1 + extension/src-webview/di.config.ts | 3 + extension/src-webview/stpa/stpa-model.ts | 1 + extension/src-webview/stpa/stpa-views.tsx | 12 ++ 8 files changed, 155 insertions(+), 89 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 9c5591b8..000fae6b 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -43,6 +43,7 @@ import { DUMMY_NODE_TYPE, EdgeType, HEADER_LABEL_TYPE, + INVISIBLE_NODE_TYPE, PARENT_TYPE, PortSide, STPAAspect, @@ -90,7 +91,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // determine the children for the STPA graph // for each component a node is generated with edges representing the references of the component // in order to be able to set the target IDs of the edges, the nodes must be created in the correct order - let stpaChildren: SModelElement[] = filteredModel.losses?.map((l) => + let stpaChildren: SModelElement[] = filteredModel.losses?.map(l => this.generateSTPANode( l, showLabels === showLabelsValue.ALL || @@ -106,7 +107,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { const sysCons = collectElementsWithSubComps(filteredModel.systemLevelConstraints); stpaChildren = stpaChildren?.concat([ ...hazards - .map((sh) => + .map(sh => this.generateAspectWithEdges( sh, showLabels === showLabelsValue.ALL || @@ -118,7 +119,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) .flat(1), ...sysCons - .map((ssc) => + .map(ssc => this.generateAspectWithEdges( ssc, showLabels === showLabelsValue.ALL || @@ -134,7 +135,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // subcomponents are contained in the parent stpaChildren = stpaChildren?.concat([ ...filteredModel.hazards - ?.map((h) => + ?.map(h => this.generateAspectWithEdges( h, showLabels === showLabelsValue.ALL || @@ -146,7 +147,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) .flat(1), ...filteredModel.systemLevelConstraints - ?.map((sc) => + ?.map(sc => this.generateAspectWithEdges( sc, showLabels === showLabelsValue.ALL || @@ -158,14 +159,14 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) .flat(1), ...filteredModel.systemLevelConstraints - ?.map((sc) => sc.subComponents?.map((ssc) => this.generateEdgesForSTPANode(ssc, args))) + ?.map(sc => sc.subComponents?.map(ssc => this.generateEdgesForSTPANode(ssc, args))) .flat(2), ]); } stpaChildren = stpaChildren?.concat([ ...filteredModel.responsibilities - ?.map((r) => - r.responsiblitiesForOneSystem.map((resp) => + ?.map(r => + r.responsiblitiesForOneSystem.map(resp => this.generateAspectWithEdges( resp, showLabels === showLabelsValue.ALL || @@ -178,10 +179,10 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) .flat(2), ...filteredModel.allUCAs - ?.map((sysUCA) => + ?.map(sysUCA => sysUCA.providingUcas .concat(sysUCA.notProvidingUcas, sysUCA.wrongTimingUcas, sysUCA.continousUcas) - .map((uca) => + .map(uca => this.generateAspectWithEdges( uca, showLabels === showLabelsValue.ALL || @@ -194,8 +195,8 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) .flat(2), ...filteredModel.rules - ?.map((rule) => - rule.contexts.map((context) => + ?.map(rule => + rule.contexts.map(context => this.generateAspectWithEdges( context, showLabels === showLabelsValue.ALL || @@ -208,7 +209,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) .flat(2), ...filteredModel.controllerConstraints - ?.map((c) => + ?.map(c => this.generateAspectWithEdges( c, showLabels === showLabelsValue.ALL || @@ -220,7 +221,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) .flat(1), ...filteredModel.scenarios - ?.map((s) => + ?.map(s => this.generateAspectWithEdges( s, showLabels === showLabelsValue.ALL || @@ -232,7 +233,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) .flat(1), ...filteredModel.safetyCons - ?.map((sr) => + ?.map(sr => this.generateAspectWithEdges( sr, showLabels === showLabelsValue.ALL || @@ -259,7 +260,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { if (filteredModel.controlStructure) { setLevelOfCSNodes(filteredModel.controlStructure?.nodes); // determine the nodes of the control structure graph - const csNodes = filteredModel.controlStructure?.nodes.map((n) => this.createControlStructureNode(n, args)); + const csNodes = filteredModel.controlStructure?.nodes.map(n => this.createControlStructureNode(n, args)); // children (nodes and edges) of the control structure const CSChildren = [ ...csNodes, @@ -295,12 +296,32 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param param1 GeneratorContext of the STPA model. * @returns A CSNode representing {@code node}. */ - protected createControlStructureNode(node: Node, { idCache }: GeneratorContext): CSNode { + protected createControlStructureNode(node: Node, args: GeneratorContext): CSNode { + const idCache = args.idCache; const label = node.label ? node.label : node.name; const nodeId = idCache.uniqueId(node.name, node); const children: SModelElement[] = this.createLabel([label], nodeId, idCache); if (this.options.getShowProcessModels()) { - children.push(...this.createCSChildren(node.variables, idCache)); + children.push(...this.createProcessModelNodes(node.variables, idCache)); + } + // add children of the control structure node + if (node.children?.length !== 0) { + const invisibleNode = { + type: INVISIBLE_NODE_TYPE, + id: idCache.uniqueId(node.name + "_invisible"), + children: [] as SModelElement[], + layout: "stack", + layoutOptions: { + paddingTop: 10.0, + paddingBottom: 10.0, + paddingLeft: 10.0, + paddingRight: 10.0, + }, + }; + node.children?.forEach(child => { + invisibleNode.children?.push(this.createControlStructureNode(child, args)); + }); + children.push(invisibleNode); } const csNode = { type: CS_NODE_TYPE, @@ -319,13 +340,16 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { return csNode; } - protected createCSChildren(variables: Variable[], idCache: IdCache): SModelElement[] { + protected createProcessModelNodes(variables: Variable[], idCache: IdCache): SModelElement[] { const csChildren: SModelElement[] = []; for (const variable of variables) { const label = variable.name; const nodeId = idCache.uniqueId(variable.name, variable); - const values = variable.values?.map((value) => value.name); - const children = [...this.createLabel([label], nodeId, idCache, HEADER_LABEL_TYPE), ...this.createLabel(values, nodeId, idCache)]; + const values = variable.values?.map(value => value.name); + const children = [ + ...this.createLabel([label], nodeId, idCache, HEADER_LABEL_TYPE), + ...this.createLabel(values, nodeId, idCache), + ]; const csNode = { type: CS_NODE_TYPE, id: nodeId, @@ -353,6 +377,12 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { const edges: (CSNode | CSEdge)[] = []; // for every control action and feedback of every a node, a edge should be created for (const node of nodes) { + const nodeId = args.idCache.getId(node); + if (nodeId) { + const snode = this.idToSNode.get(nodeId); + // if a cs node has children they are encapsulated in an invisible node + snode?.children?.find(node => node.type === INVISIBLE_NODE_TYPE)?.children?.push(...this.generateVerticalCSEdges(node.children, args)); + } // create edges representing the control actions edges.push(...this.translateCommandsToEdges(node.actions, EdgeType.CONTROL_ACTION, args)); // create edges representing feedback @@ -920,10 +950,15 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param id The ID of the element for which the label should be generated. * @returns SLabel elements representing {@code label}. */ - protected createLabel(label: string[], id: string, idCache: IdCache, type: string = "label:xref"): SLabel[] { + protected createLabel( + label: string[], + id: string, + idCache: IdCache, + type: string = "label:xref" + ): SLabel[] { const children: SLabel[] = []; - if (label.find((l) => l !== "")) { - label.forEach((l) => { + if (label.find(l => l !== "")) { + label.forEach(l => { children.push({ type: type, id: idCache.uniqueId(id + "_label"), diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index 0dfde42c..43248778 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -16,20 +16,18 @@ */ /* eslint-disable @typescript-eslint/no-unused-vars */ -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 { CS_NODE_TYPE, PARENT_TYPE, PortSide, STPA_NODE_TYPE, STPA_PORT_TYPE } from './stpa-model'; - +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 { CS_NODE_TYPE, INVISIBLE_NODE_TYPE, PARENT_TYPE, PortSide, STPA_NODE_TYPE, STPA_PORT_TYPE } from "./stpa-model"; export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { - protected graphOptions(sgraph: SGraph, index: SModelIndex): LayoutOptions { // options for the whole graph containing the control structure and the STPA graph return { - 'org.eclipse.elk.partitioning.activate': 'true', - 'org.eclipse.elk.direction': 'DOWN' + "org.eclipse.elk.partitioning.activate": "true", + "org.eclipse.elk.direction": "DOWN", }; } @@ -37,37 +35,37 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { * Options for the parent nodes of the STPA graph and the control structure */ protected grandparentNodeOptions(snode: ParentNode, index: SModelIndex): LayoutOptions { - let direction = ''; + let direction = ""; // priority is used to determine the order of the nodes - let priority = ''; + let priority = ""; if (snode.children?.find(child => child.type === CS_NODE_TYPE)) { // options for the control structure - direction = 'DOWN'; - priority = '1'; + direction = "DOWN"; + priority = "1"; } else if (snode.children?.find(child => child.type === STPA_NODE_TYPE)) { // options for the STPA graph - direction = 'UP'; - priority = '0'; + direction = "UP"; + priority = "0"; } const options: LayoutOptions = { - 'org.eclipse.elk.layered.thoroughness': '70', - 'org.eclipse.elk.partitioning.activate': 'true', - 'org.eclipse.elk.direction': direction, - // nodes with many edges are streched - 'org.eclipse.elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX', - 'org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeFlexibility.default': 'NODE_SIZE', - 'org.eclipse.elk.spacing.portPort': '10', + "org.eclipse.elk.layered.thoroughness": "70", + "org.eclipse.elk.partitioning.activate": "true", + "org.eclipse.elk.direction": direction, + // nodes with many edges are streched + "org.eclipse.elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", + "org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeFlexibility.default": "NODE_SIZE", + "org.eclipse.elk.spacing.portPort": "10", // edges do no start at the border of the node - 'org.eclipse.elk.spacing.portsSurrounding': '[top=10.0,left=10.0,bottom=10.0,right=10.0]', - 'org.eclipse.elk.priority': priority, + "org.eclipse.elk.spacing.portsSurrounding": "[top=10.0,left=10.0,bottom=10.0,right=10.0]", + "org.eclipse.elk.priority": priority, }; // model order is used to determine the order of the children if (snode.modelOrder) { - options['org.eclipse.elk.layered.considerModelOrder.strategy'] = 'NODES_AND_EDGES'; - options['org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder'] = 'true'; - options['org.eclipse.elk.separateConnectedComponents'] = 'false'; + options["org.eclipse.elk.layered.considerModelOrder.strategy"] = "NODES_AND_EDGES"; + options["org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder"] = "true"; + options["org.eclipse.elk.separateConnectedComponents"] = "false"; } return options; @@ -77,6 +75,8 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { switch (snode.type) { case CS_NODE_TYPE: return this.csNodeOptions(snode as CSNode); + case INVISIBLE_NODE_TYPE: + return this.invisibleNodeOptions(snode); case STPA_NODE_TYPE: return this.stpaNodeOptions(snode as STPANode); case PARENT_TYPE: @@ -84,15 +84,22 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { } } + protected invisibleNodeOptions(snode: SNode): LayoutOptions | undefined { + return { + "org.eclipse.elk.partitioning.activate": "true", + "org.eclipse.elk.direction": "DOWN", + }; + } + protected stpaNodeOptions(node: STPANode): LayoutOptions { - if (node.children?.find(child => child.type.startsWith('node'))) { + if (node.children?.find(child => child.type.startsWith("node"))) { return this.parentSTPANodeOptions(node); } else { return { - 'org.eclipse.elk.nodeLabels.placement': "INSIDE V_CENTER H_CENTER", - 'org.eclipse.elk.partitioning.partition': "" + node.level, - 'org.eclipse.elk.portConstraints': 'FIXED_SIDE', - 'org.eclipse.elk.nodeSize.constraints': 'NODE_LABELS', + "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", + "org.eclipse.elk.partitioning.partition": "" + node.level, + "org.eclipse.elk.portConstraints": "FIXED_SIDE", + "org.eclipse.elk.nodeSize.constraints": "NODE_LABELS", }; } } @@ -100,69 +107,70 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { protected parentSTPANodeOptions(node: STPANode): LayoutOptions { // options for nodes in the STPA graphs that have children const options: LayoutOptions = { - 'org.eclipse.elk.direction': 'UP', - 'org.eclipse.elk.nodeLabels.placement': "INSIDE V_TOP H_CENTER", - 'org.eclipse.elk.partitioning.partition': "" + node.level, - 'org.eclipse.elk.nodeSize.constraints': 'NODE_LABELS', - // nodes with many edges are streched - 'org.eclipse.elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX', - 'org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeFlexibility.default': 'NODE_SIZE', + "org.eclipse.elk.direction": "UP", + "org.eclipse.elk.nodeLabels.placement": "INSIDE V_TOP H_CENTER", + "org.eclipse.elk.partitioning.partition": "" + node.level, + "org.eclipse.elk.nodeSize.constraints": "NODE_LABELS", + // nodes with many edges are streched + "org.eclipse.elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", + "org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeFlexibility.default": "NODE_SIZE", // edges do no start at the border of the node - 'org.eclipse.elk.spacing.portsSurrounding': '[top=10.0,left=10.0,bottom=10.0,right=10.0]', - 'org.eclipse.elk.portConstraints': 'FIXED_SIDE' + "org.eclipse.elk.spacing.portsSurrounding": "[top=10.0,left=10.0,bottom=10.0,right=10.0]", + "org.eclipse.elk.portConstraints": "FIXED_SIDE", }; // model order is used to determine the order of the children if (node.modelOrder) { - options['org.eclipse.elk.layered.considerModelOrder.strategy'] = 'NODES_AND_EDGES'; - options['org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder'] = 'true'; - options['org.eclipse.elk.separateConnectedComponents'] = 'false'; + options["org.eclipse.elk.layered.considerModelOrder.strategy"] = "NODES_AND_EDGES"; + options["org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder"] = "true"; + options["org.eclipse.elk.separateConnectedComponents"] = "false"; } return options; } protected csNodeOptions(node: CSNode): LayoutOptions { const options: LayoutOptions = { - 'org.eclipse.elk.partitioning.partition': "" + node.level, - 'org.eclipse.elk.nodeSize.constraints': 'NODE_LABELS', + "org.eclipse.elk.partitioning.partition": "" + node.level, + "org.eclipse.elk.nodeSize.constraints": "NODE_LABELS", // edges do no start at the border of the node - 'org.eclipse.elk.spacing.portsSurrounding': '[top=10.0,left=10.0,bottom=10.0,right=10.0]', - 'org.eclipse.elk.portConstraints': 'FIXED_SIDE', + "org.eclipse.elk.spacing.portsSurrounding": "[top=10.0,left=10.0,bottom=10.0,right=10.0]", + "org.eclipse.elk.portConstraints": "FIXED_SIDE", }; - if (node.children?.find(child => child.type.startsWith('node'))) { - options['org.eclipse.elk.nodeLabels.placement'] = "INSIDE V_TOP H_CENTER"; + if (node.children?.find(child => child.type.startsWith("node"))) { + // cs nodes with children + options["org.eclipse.elk.nodeLabels.placement"] = "INSIDE V_TOP H_CENTER"; // TODO: want to use rectpacking instead of layered but this does not work at the moment (sprotty issue) - options['org.eclipse.elk.direction'] = 'DOWN'; - options['org.eclipse.elk.separateConnectedComponents'] = 'false'; + // options['org.eclipse.elk.algorithm'] = 'RECTPACKING'; + options["org.eclipse.elk.direction"] = "DOWN"; + options["org.eclipse.elk.separateConnectedComponents"] = "false"; + options["org.eclipse.elk.partitioning.activate"] = "true"; } else { // TODO: want H_LEFT but this expands the node more than needed - options['org.eclipse.elk.nodeLabels.placement'] = "INSIDE V_CENTER H_CENTER"; + options["org.eclipse.elk.nodeLabels.placement"] = "INSIDE V_CENTER H_CENTER"; } return options; } protected portOptions(sport: SPort, index: SModelIndex): LayoutOptions | undefined { if (sport.type === STPA_PORT_TYPE) { - let side = ''; + let side = ""; switch ((sport as STPAPort).side) { case PortSide.WEST: - side = 'WEST'; + side = "WEST"; break; case PortSide.EAST: - side = 'EAST'; + side = "EAST"; break; case PortSide.NORTH: - side = 'NORTH'; + side = "NORTH"; break; case PortSide.SOUTH: - side = 'SOUTH'; + side = "SOUTH"; break; } return { - 'org.eclipse.elk.port.side': side + "org.eclipse.elk.port.side": side, }; } - } - } diff --git a/extension/src-language-server/stpa/diagram/stpa-model.ts b/extension/src-language-server/stpa/diagram/stpa-model.ts index 9ad1525d..0747febe 100644 --- a/extension/src-language-server/stpa/diagram/stpa-model.ts +++ b/extension/src-language-server/stpa/diagram/stpa-model.ts @@ -19,6 +19,7 @@ export const STPA_NODE_TYPE = 'node:stpa'; export const PARENT_TYPE= 'node:parent'; export const CS_NODE_TYPE = 'node:cs'; +export const INVISIBLE_NODE_TYPE = 'node:invisible'; export const DUMMY_NODE_TYPE = 'node:dummy'; export const EDGE_TYPE = 'edge'; export const CS_EDGE_TYPE = 'edge:controlStructure'; diff --git a/extension/src-language-server/stpa/diagram/utils.ts b/extension/src-language-server/stpa/diagram/utils.ts index d9b819cd..c3ea6943 100644 --- a/extension/src-language-server/stpa/diagram/utils.ts +++ b/extension/src-language-server/stpa/diagram/utils.ts @@ -69,7 +69,7 @@ export function getTargets(node: AstNode, hierarchy: boolean): AstNode[] { } else if (isLossScenario(node) && node.uca && node.uca.ref) { targets.push(node.uca.ref); } else if ((isUCA(node) || isContext(node) || isLossScenario(node)) && node.list) { - const refs = node.list.refs.map((x) => x.ref); + const refs = node.list.refs.map(x => x.ref); for (const ref of refs) { if (ref) { targets.push(ref); @@ -178,9 +178,14 @@ export function setLevelOfCSNodes(nodes: Node[]): void { const visited = new Map>(); for (const node of nodes) { visited.set(node.name, new Set()); + if (node.children) { + setLevelOfCSNodes(node.children); + } + } + if (nodes.length > 0) { + nodes[0].level = 0; + assignLevel(nodes[0], visited); } - nodes[0].level = 0; - assignLevel(nodes[0], visited); } /** @@ -312,7 +317,7 @@ export function getCurrentAspect(model: Model): STPAAspect { ]; // find first element that is after the cursor position and return the aspect of the previous element - const index = elements.findIndex((element) => element.$cstNode && element.$cstNode.offset >= currentCursorOffset) - 1; + const index = elements.findIndex(element => element.$cstNode && element.$cstNode.offset >= currentCursorOffset) - 1; if (index < 0) { return STPAAspect.LOSS; } diff --git a/extension/src-language-server/stpa/stpa.langium b/extension/src-language-server/stpa/stpa.langium index e54c4306..801a3955 100644 --- a/extension/src-language-server/stpa/stpa.langium +++ b/extension/src-language-server/stpa/stpa.langium @@ -69,6 +69,7 @@ Node: ('output' '[' outputs+=Command (',' outputs+=Command)* ']')? ('controlActions' '{'actions+=VerticalEdge*'}')? ('feedback' '{'feedbacks+=VerticalEdge*'}')? + (children+=Node)* '}'; /* Edge: diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index a7d4b06f..4b3dcf96 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -56,6 +56,7 @@ import { CS_NODE_TYPE, DUMMY_NODE_TYPE, HEADER_LABEL_TYPE, + INVISIBLE_NODE_TYPE, PARENT_TYPE, STPAEdge, STPANode, @@ -70,6 +71,7 @@ import { CSNodeView, HeaderLabelView, IntermediateEdgeView, + InvisibleNodeView, PolylineArrowEdgeView, PortView, STPAGraphView, @@ -104,6 +106,7 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = // STPA configureModelElement(context, "graph", SGraph, STPAGraphView); + configureModelElement(context, INVISIBLE_NODE_TYPE, SNode, InvisibleNodeView); configureModelElement(context, DUMMY_NODE_TYPE, CSNode, CSNodeView); configureModelElement(context, CS_NODE_TYPE, CSNode, CSNodeView); configureModelElement(context, STPA_NODE_TYPE, STPANode, STPANodeView); diff --git a/extension/src-webview/stpa/stpa-model.ts b/extension/src-webview/stpa/stpa-model.ts index 647e9d2f..d43f07bc 100644 --- a/extension/src-webview/stpa/stpa-model.ts +++ b/extension/src-webview/stpa/stpa-model.ts @@ -21,6 +21,7 @@ import { SEdge, SNode, SPort, connectableFeature, fadeFeature, layoutContainerFe export const STPA_NODE_TYPE = 'node:stpa'; export const PARENT_TYPE = 'node:parent'; export const CS_NODE_TYPE = 'node:cs'; +export const INVISIBLE_NODE_TYPE = 'node:invisible'; export const DUMMY_NODE_TYPE = 'node:dummy'; export const EDGE_TYPE = 'edge'; export const CS_EDGE_TYPE = 'edge:controlStructure'; diff --git a/extension/src-webview/stpa/stpa-views.tsx b/extension/src-webview/stpa/stpa-views.tsx index 7dd33bc7..dc92cac0 100644 --- a/extension/src-webview/stpa/stpa-views.tsx +++ b/extension/src-webview/stpa/stpa-views.tsx @@ -218,6 +218,18 @@ export class CSNodeView extends RectangularNodeView { } } +@injectable() +export class InvisibleNodeView extends RectangularNodeView { + + @inject(DISymbol.RenderOptionsRegistry) renderOptionsRegistry: RenderOptionsRegistry; + + render(node: SNode, context: RenderingContext): VNode { + return + {context.renderChildren(node)} + ; + } +} + @injectable() export class STPAGraphView extends SGraphView { From 1142f5fb0e98d877c1a02470dd8e2efd37748c5c Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 7 Feb 2024 16:24:41 +0100 Subject: [PATCH 03/16] hierarchy crossing edges (WIP) currently only control actions work not feedback --- .../stpa/diagram/diagram-generator.ts | 174 ++++++++++++++---- .../stpa/diagram/stpa-model.ts | 1 + .../src-language-server/stpa/diagram/utils.ts | 27 ++- extension/src-webview/di.config.ts | 2 + extension/src-webview/stpa/stpa-model.ts | 1 + 5 files changed, 165 insertions(+), 40 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 000fae6b..9b9ef9a8 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -21,6 +21,7 @@ import { SLabel, SModelElement, SModelRoot, SNode } from "sprotty-protocol"; import { Command, + Graph, Hazard, Model, Node, @@ -39,6 +40,7 @@ import { filterModel } from "./filtering"; import { CSEdge, CSNode, ParentNode, STPAEdge, STPANode, STPAPort } from "./stpa-interfaces"; import { CS_EDGE_TYPE, + CS_INTERMEDIATE_EDGE_TYPE, CS_NODE_TYPE, DUMMY_NODE_TYPE, EdgeType, @@ -57,6 +59,7 @@ import { createUCAContextDescription, getAspect, getAspectsThatShouldHaveDesriptions, + getCommonAncestor, getTargets, setLevelOfCSNodes, setLevelsForSTPANodes, @@ -377,12 +380,8 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { const edges: (CSNode | CSEdge)[] = []; // for every control action and feedback of every a node, a edge should be created for (const node of nodes) { - const nodeId = args.idCache.getId(node); - if (nodeId) { - const snode = this.idToSNode.get(nodeId); - // if a cs node has children they are encapsulated in an invisible node - snode?.children?.find(node => node.type === INVISIBLE_NODE_TYPE)?.children?.push(...this.generateVerticalCSEdges(node.children, args)); - } + // create edges for children and add the ones that must be added at the top level + edges.push(...this.generateVerticalCSEdges(node.children, args)); // create edges representing the control actions edges.push(...this.translateCommandsToEdges(node.actions, EdgeType.CONTROL_ACTION, args)); // create edges representing feedback @@ -398,45 +397,112 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { /** * Translates the commands (control action or feedback) of a node to edges. * @param commands The control actions or feedback of a node. - * @param edgetype The type of the edge (control action or feedback). + * @param edgeType The type of the edge (control action or feedback). * @param args GeneratorContext of the STPA model. * @returns A list of edges representing the commands. */ protected translateCommandsToEdges( commands: VerticalEdge[], - edgetype: EdgeType, + edgeType: EdgeType, args: GeneratorContext ): CSEdge[] { const idCache = args.idCache; const edges: CSEdge[] = []; for (const edge of commands) { - const sourceId = idCache.getId(edge.$container); - const targetId = idCache.getId(edge.target.ref); + const source = edge.$container; + const target = edge.target.ref; + const sourceId = idCache.getId(source); + const targetId = idCache.getId(target); const edgeId = idCache.uniqueId(`${sourceId}_${edge.comms[0].name}_${targetId}`, edge); - // multiple commands to same target is represented by one edge - const label: string[] = []; - for (let i = 0; i < edge.comms.length; i++) { - const com = edge.comms[i]; - label.push(com.label); - } - const portIds = this.createPortsForEdge( - sourceId ?? "", - edgetype === EdgeType.CONTROL_ACTION ? PortSide.SOUTH : PortSide.NORTH, - targetId ?? "", - edgetype === EdgeType.CONTROL_ACTION ? PortSide.NORTH : PortSide.SOUTH, - edgeId, - idCache - ); - const e = this.createControlStructureEdge( - edgeId, - portIds.sourcePortId, - portIds.targetPortId, - label, - edgetype, - args - ); - edges.push(e); + // TODO: intermediate edges if source and target are in different hierarchies + if (target && sourceId) { + const commonAncestor = getCommonAncestor(source, target); + + if (edgeType === EdgeType.CONTROL_ACTION) { + const sourcePortIds = this.generatePortsForCSHierarchy( + source, + edgeId, + PortSide.SOUTH, + idCache, + commonAncestor + ); + const targetPortIds = this.generatePortsForCSHierarchy( + target, + edgeId, + PortSide.NORTH, + idCache, + commonAncestor + ); + + // add edges between the ports + for (let i = 0; i < sourcePortIds.nodes.length - 1; i++) { + const sEdgeType = CS_INTERMEDIATE_EDGE_TYPE; + sourcePortIds.nodes[i + 1]?.children?.push( + this.createControlStructureEdge( + idCache.uniqueId(edgeId), + sourcePortIds.portIds[i], + sourcePortIds.portIds[i + 1], + [], + edgeType, + sEdgeType, + args + ) + ); + } + // add edges between the ports + for (let i = 0; i < targetPortIds.nodes.length - 1; i++) { + const sEdgeType = i === 0 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE; + targetPortIds.nodes[i + 1]?.children?.push( + this.createControlStructureEdge( + idCache.uniqueId(edgeId), + targetPortIds.portIds[i + 1], + targetPortIds.portIds[i], + [], + edgeType, + sEdgeType, + args + ) + ); + } + + // multiple commands to same target is represented by one edge + const label: string[] = []; + for (let i = 0; i < edge.comms.length; i++) { + const com = edge.comms[i]; + label.push(com.label); + } + // edge between the two ports in the common ancestor + if (commonAncestor?.$type === "Graph") { + const e = this.createControlStructureEdge( + edgeId, + sourcePortIds.portIds[sourcePortIds.portIds.length - 1], + targetPortIds.portIds[targetPortIds.portIds.length - 1], + label, + edgeType, + targetPortIds.portIds.length === 1 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE, + args + ); + edges.push(e); + } else { + const snodeAncestor = this.idToSNode.get(idCache.getId(commonAncestor!)!); + snodeAncestor?.children + ?.find(node => node.type === INVISIBLE_NODE_TYPE) + ?.children?.push( + this.createControlStructureEdge( + idCache.uniqueId(edgeId), + sourcePortIds.portIds[sourcePortIds.portIds.length - 1], + targetPortIds.portIds[targetPortIds.portIds.length - 1], + label, + edgeType, + targetPortIds.portIds.length === 1 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE, + args + ) + ); + } + } else if (edgeType === EdgeType.FEEDBACK) { + } + } } return edges; } @@ -512,6 +578,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { nodeId ? nodeId : "", label, edgetype, + CS_EDGE_TYPE, args ); graphComponents = [inputEdge, inputDummyNode]; @@ -530,6 +597,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { outputDummyNode.id ? outputDummyNode.id : "", label, edgetype, + CS_EDGE_TYPE, args ); graphComponents = [outputEdge, outputDummyNode]; @@ -678,7 +746,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { if ((isHazard(target) || isSystemConstraint(target)) && target.$container?.$type !== "Model") { // if the target is a subcomponent we need to add several ports and edges through the hierarchical structure - return this.generateIntermediateIncomingEdges(target, source, sourceId, edgeId, children, idCache); + return this.generateIntermediateIncomingSTPAEdges(target, source, sourceId, edgeId, children, idCache); } else { // otherwise it is sufficient to add ports for source and target const portIds = this.createPortsForEdge( @@ -713,7 +781,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param idCache The ID cache of the STPA model. * @returns an STPAEdge to connect the {@code source} (or its top parent) with the top parent of the {@code target}. */ - protected generateIntermediateIncomingEdges( + protected generateIntermediateIncomingSTPAEdges( target: AstNode, source: AstNode, sourceId: string, @@ -744,7 +812,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { if (isSystemConstraint(source) && source.$container?.$type !== "Model") { // if the source is a sub-sytemconstraint we also need intermediate edges to the top system constraint - return this.generateIntermediateOutgoingEdges( + return this.generateIntermediateOutgoingSTPAEdges( source, edgeId, children, @@ -778,7 +846,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param idCache The ID cache of the STPA model. * @returns the STPAEdge to connect the top parent of the {@code source} with the {@code targetPortId}. */ - protected generateIntermediateOutgoingEdges( + protected generateIntermediateOutgoingSTPAEdges( source: AstNode, edgeId: string, children: SModelElement[], @@ -841,6 +909,35 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { return ids; } + protected generatePortsForCSHierarchy( + current: AstNode | undefined, + edgeId: string, + side: PortSide, + idCache: IdCache, + ancestor?: Node | Graph + ): { portIds: string[]; nodes: SNode[] } { + const ids: string[] = []; + const nodes: SNode[] = []; + while (current && (!ancestor || current !== ancestor)) { + const currentId = idCache.getId(current); + const currentNode = this.idToSNode.get(currentId!); + const invisibleChild = currentNode?.children?.find(child => child.type === INVISIBLE_NODE_TYPE); + if (invisibleChild && ids.length !== 0) { + // add port for the invisible node first + const portId = idCache.uniqueId(edgeId + "_newTransition"); + invisibleChild.children?.push(this.createSTPAPort(portId, side)); + ids.push(portId); + nodes.push(invisibleChild); + } + const portId = idCache.uniqueId(edgeId + "_newTransition"); + currentNode?.children?.push(this.createSTPAPort(portId, side)); + ids.push(portId); + nodes.push(currentNode!); + current = current?.$container; + } + return { portIds: ids, nodes: nodes }; + } + /** * Creates an STPANode. * @param node The AstNode for which the STPANode should be created. @@ -932,10 +1029,11 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { targetId: string, label: string[], edgeType: EdgeType, + sedgeType: string, args: GeneratorContext ): CSEdge { return { - type: CS_EDGE_TYPE, + type: sedgeType, id: edgeId, sourceId: sourceId!, targetId: targetId!, diff --git a/extension/src-language-server/stpa/diagram/stpa-model.ts b/extension/src-language-server/stpa/diagram/stpa-model.ts index 0747febe..9f91b003 100644 --- a/extension/src-language-server/stpa/diagram/stpa-model.ts +++ b/extension/src-language-server/stpa/diagram/stpa-model.ts @@ -25,6 +25,7 @@ export const EDGE_TYPE = 'edge'; 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 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 c3ea6943..addc0fcd 100644 --- a/extension/src-language-server/stpa/diagram/utils.ts +++ b/extension/src-language-server/stpa/diagram/utils.ts @@ -18,6 +18,7 @@ import { AstNode } from "langium"; import { Context, + Graph, Model, Node, isActionUCAs, @@ -196,7 +197,7 @@ export function setLevelOfCSNodes(nodes: Node[]): void { function assignLevel(node: Node, visited: Map>): void { for (const action of node.actions) { const target = action.target.ref; - if (target && !visited.get(node.name)?.has(target.name)) { + if (getCommonAncestor(node, target!) === node.$container && target && !visited.get(node.name)?.has(target.name)) { visited.get(node.name)?.add(target.name); if (target.level === undefined || target.level < node.level! + 1) { target.level = node.level! + 1; @@ -206,7 +207,7 @@ function assignLevel(node: Node, visited: Map>): void { } for (const feedback of node.feedbacks) { const target = feedback.target.ref; - if (target && !visited.get(node.name)?.has(target.name)) { + if (getCommonAncestor(node, target!) === node.$container && target && !visited.get(node.name)?.has(target.name)) { visited.get(node.name)?.add(target.name); if (target.level === undefined || target.level > node.level! - 1) { target.level = node.level! - 1; @@ -362,3 +363,25 @@ export function getAspectsThatShouldHaveDesriptions(model: Model): STPAAspect[] } return aspectsToShowDescriptions; } + + +export function getCommonAncestor(node: Node, target: Node): Node|Graph | undefined { + const nodeAncestors = getAncestors(node); + const targetAncestors = getAncestors(target); + for (const ancestor of nodeAncestors) { + if (targetAncestors.includes(ancestor)) { + return ancestor; + } + } + return undefined; +} + +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 diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index 4b3dcf96..a2e4a989 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -53,6 +53,7 @@ import { CSEdge, CSNode, CS_EDGE_TYPE, + CS_INTERMEDIATE_EDGE_TYPE, CS_NODE_TYPE, DUMMY_NODE_TYPE, HEADER_LABEL_TYPE, @@ -113,6 +114,7 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = configureModelElement(context, PARENT_TYPE, SNode, CSNodeView); configureModelElement(context, STPA_EDGE_TYPE, STPAEdge, PolylineArrowEdgeView); 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); diff --git a/extension/src-webview/stpa/stpa-model.ts b/extension/src-webview/stpa/stpa-model.ts index d43f07bc..8203d4e1 100644 --- a/extension/src-webview/stpa/stpa-model.ts +++ b/extension/src-webview/stpa/stpa-model.ts @@ -27,6 +27,7 @@ export const EDGE_TYPE = 'edge'; 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 HEADER_LABEL_TYPE = 'label:header'; From 16c54f989a3027d7e90760efb3983562fa1b18ea Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 8 Feb 2024 10:40:14 +0100 Subject: [PATCH 04/16] fixed scoping --- .../stpa/stpa-scopeProvider.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/extension/src-language-server/stpa/stpa-scopeProvider.ts b/extension/src-language-server/stpa/stpa-scopeProvider.ts index 8779c687..9e761c9e 100644 --- a/extension/src-language-server/stpa/stpa-scopeProvider.ts +++ b/extension/src-language-server/stpa/stpa-scopeProvider.ts @@ -27,7 +27,7 @@ import { getDocument, stream } from "langium"; -import { ActionUCAs, Command, Context, DCAContext, DCARule, Hazard, LossScenario, Model, Node, Rule, SystemConstraint, UCA, Variable, isActionUCAs, isControllerConstraint, isContext, isDCAContext, isDCARule, isHazardList, isLossScenario, isModel, isResponsibility, isSystemResponsibilities, isRule, isSafetyConstraint, isSystemConstraint } from "../generated/ast"; +import { ActionUCAs, Command, Context, DCAContext, DCARule, Hazard, LossScenario, Model, Node, Rule, SystemConstraint, UCA, Variable, isActionUCAs, isControllerConstraint, isContext, isDCAContext, isDCARule, isHazardList, isLossScenario, isModel, isResponsibility, isSystemResponsibilities, isRule, isSafetyConstraint, isSystemConstraint, isNode, isVerticalEdge, VerticalEdge, isGraph, Graph } from "../generated/ast"; import { StpaServices } from "./stpa-module"; @@ -40,6 +40,7 @@ export class StpaScopeProvider extends DefaultScopeProvider { private UCA_TYPE = UCA; private CONTEXT_TYPE = Context; private VAR_TYPE = Variable; + private NODE_TYPE = Node; constructor(services: StpaServices) { super(services); @@ -68,6 +69,8 @@ export class StpaScopeProvider extends DefaultScopeProvider { return this.getCAs(node, precomputed); } else if ((isContext(node) || isDCAContext(node)) && referenceType === this.VAR_TYPE) { return this.getVars(node, precomputed); + } else if (isVerticalEdge(node) && referenceType === this.NODE_TYPE) { + return this.getNodes(node, precomputed); } else { return this.getStandardScope(node, referenceType, precomputed); } @@ -129,6 +132,28 @@ export class StpaScopeProvider extends DefaultScopeProvider { return this.descriptionsToScope(allDescriptions); } + protected getNodes(node: VerticalEdge, precomputed: PrecomputedScopes): Scope { + let graph: Node | Graph = node.$container; + while (graph && !isGraph(graph)) { + graph = graph.$container; + } + + const allDescriptions = this.getChildrenNodes(graph.nodes, precomputed); + return this.descriptionsToScope(allDescriptions); + } + + getChildrenNodes(nodes: Node[], precomputed: PrecomputedScopes): AstNodeDescription[] { + let res: AstNodeDescription[] = []; + for (const node of nodes) { + const currentNode: AstNode | undefined = node; + if (node.children.length !== 0) { + res = res.concat(this.getChildrenNodes(node.children, precomputed)); + } + res = res.concat(this.getDescriptions(currentNode, this.NODE_TYPE, precomputed)); + } + return res; + } + /** * Creates scope containing the variables of the system component referenced by {@code node}. * @param node Current Rule. From 36d579bbe978e1b74b2c2ce1592d3b44f834e7ae Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 8 Feb 2024 13:39:01 +0100 Subject: [PATCH 05/16] refactoring + works now also for feedback edges --- .../stpa/diagram/diagram-generator.ts | 192 +++++++++--------- 1 file changed, 91 insertions(+), 101 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 9b9ef9a8..5a3be9bc 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -411,96 +411,37 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { for (const edge of commands) { const source = edge.$container; const target = edge.target.ref; - const sourceId = idCache.getId(source); - const targetId = idCache.getId(target); - const edgeId = idCache.uniqueId(`${sourceId}_${edge.comms[0].name}_${targetId}`, edge); + const edgeId = idCache.uniqueId( + `${idCache.getId(source)}_${edge.comms[0].name}_${idCache.getId(target)}`, + edge + ); - // TODO: intermediate edges if source and target are in different hierarchies - if (target && sourceId) { + if (target) { + // multiple commands to same target is represented by one edge + const label: string[] = []; + for (let i = 0; i < edge.comms.length; i++) { + const com = edge.comms[i]; + label.push(com.label); + } + // edges can be hierachy crossing so we must determine the common ancestor const commonAncestor = getCommonAncestor(source, target); - - if (edgeType === EdgeType.CONTROL_ACTION) { - const sourcePortIds = this.generatePortsForCSHierarchy( - source, - edgeId, - PortSide.SOUTH, - idCache, - commonAncestor - ); - const targetPortIds = this.generatePortsForCSHierarchy( - target, - edgeId, - PortSide.NORTH, - idCache, - commonAncestor - ); - - // add edges between the ports - for (let i = 0; i < sourcePortIds.nodes.length - 1; i++) { - const sEdgeType = CS_INTERMEDIATE_EDGE_TYPE; - sourcePortIds.nodes[i + 1]?.children?.push( - this.createControlStructureEdge( - idCache.uniqueId(edgeId), - sourcePortIds.portIds[i], - sourcePortIds.portIds[i + 1], - [], - edgeType, - sEdgeType, - args - ) - ); - } - // add edges between the ports - for (let i = 0; i < targetPortIds.nodes.length - 1; i++) { - const sEdgeType = i === 0 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE; - targetPortIds.nodes[i + 1]?.children?.push( - this.createControlStructureEdge( - idCache.uniqueId(edgeId), - targetPortIds.portIds[i + 1], - targetPortIds.portIds[i], - [], - edgeType, - sEdgeType, - args - ) - ); - } - - // multiple commands to same target is represented by one edge - const label: string[] = []; - for (let i = 0; i < edge.comms.length; i++) { - const com = edge.comms[i]; - label.push(com.label); - } - // edge between the two ports in the common ancestor - if (commonAncestor?.$type === "Graph") { - const e = this.createControlStructureEdge( - edgeId, - sourcePortIds.portIds[sourcePortIds.portIds.length - 1], - targetPortIds.portIds[targetPortIds.portIds.length - 1], - label, - edgeType, - targetPortIds.portIds.length === 1 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE, - args - ); - edges.push(e); - } else { - const snodeAncestor = this.idToSNode.get(idCache.getId(commonAncestor!)!); - snodeAncestor?.children - ?.find(node => node.type === INVISIBLE_NODE_TYPE) - ?.children?.push( - this.createControlStructureEdge( - idCache.uniqueId(edgeId), - sourcePortIds.portIds[sourcePortIds.portIds.length - 1], - targetPortIds.portIds[targetPortIds.portIds.length - 1], - label, - edgeType, - targetPortIds.portIds.length === 1 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE, - args - ) - ); - } - } else if (edgeType === EdgeType.FEEDBACK) { + // create the intermediate ports and edges for the control action + const ports = this.generateIntermediateCSEdges(source, target, edgeId, edgeType, args, commonAncestor); + // add edge between the two ports in the common ancestor + const csEdge = this.createControlStructureEdge( + idCache.uniqueId(edgeId), + ports.sourcePort, + ports.targetPort, + label, + edgeType, + target.$container === commonAncestor ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE, + args + ); + if (commonAncestor?.$type === "Graph") { + edges.push(csEdge); + } else if (commonAncestor) { + const snodeAncestor = this.idToSNode.get(idCache.getId(commonAncestor)!); + snodeAncestor?.children?.find(node => node.type === INVISIBLE_NODE_TYPE)?.children?.push(csEdge); } } } @@ -909,6 +850,51 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { return ids; } + protected generateIntermediateCSEdges( + source: AstNode | undefined, + target: AstNode | 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); + for (let i = 0; i < sources.nodes.length - 1; i++) { + const sEdgeType = CS_INTERMEDIATE_EDGE_TYPE; + sources.nodes[i + 1]?.children?.push( + this.createControlStructureEdge( + args.idCache.uniqueId(edgeId), + sources.portIds[i], + sources.portIds[i + 1], + [], + edgeType, + sEdgeType, + args + ) + ); + } + for (let i = 0; i < targets.nodes.length - 1; i++) { + const sEdgeType = i == 0 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE; + targets.nodes[i + 1]?.children?.push( + this.createControlStructureEdge( + args.idCache.uniqueId(edgeId), + targets.portIds[i + 1], + targets.portIds[i], + [], + edgeType, + sEdgeType, + args + ) + ); + } + return { + sourcePort: sources.portIds[sources.portIds.length - 1], + targetPort: targets.portIds[targets.portIds.length - 1], + }; + } + + // adds ports for current node and its (grand)parents up to the ancestor. The ancestor get no port. protected generatePortsForCSHierarchy( current: AstNode | undefined, edgeId: string, @@ -920,20 +906,24 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { const nodes: SNode[] = []; while (current && (!ancestor || current !== ancestor)) { const currentId = idCache.getId(current); - const currentNode = this.idToSNode.get(currentId!); - const invisibleChild = currentNode?.children?.find(child => child.type === INVISIBLE_NODE_TYPE); - if (invisibleChild && ids.length !== 0) { - // add port for the invisible node first - const portId = idCache.uniqueId(edgeId + "_newTransition"); - invisibleChild.children?.push(this.createSTPAPort(portId, side)); - ids.push(portId); - nodes.push(invisibleChild); + if (currentId) { + const currentNode = this.idToSNode.get(currentId); + if (currentNode) { + const invisibleChild = currentNode?.children?.find(child => child.type === INVISIBLE_NODE_TYPE); + if (invisibleChild && ids.length !== 0) { + // add port for the invisible node first + const invisiblePortId = idCache.uniqueId(edgeId + "_newTransition"); + invisibleChild.children?.push(this.createSTPAPort(invisiblePortId, side)); + ids.push(invisiblePortId); + nodes.push(invisibleChild); + } + const nodePortId = idCache.uniqueId(edgeId + "_newTransition"); + currentNode?.children?.push(this.createSTPAPort(nodePortId, side)); + ids.push(nodePortId); + nodes.push(currentNode); + current = current?.$container; + } } - const portId = idCache.uniqueId(edgeId + "_newTransition"); - currentNode?.children?.push(this.createSTPAPort(portId, side)); - ids.push(portId); - nodes.push(currentNode!); - current = current?.$container; } return { portIds: ids, nodes: nodes }; } From 3b1318d3e451271d04860587d1c397acd07a067f Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 8 Feb 2024 13:39:11 +0100 Subject: [PATCH 06/16] adjusted view and automatic level determination --- extension/src-language-server/stpa/diagram/utils.ts | 4 ++-- extension/src-webview/stpa/stpa-views.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/utils.ts b/extension/src-language-server/stpa/diagram/utils.ts index addc0fcd..335d8303 100644 --- a/extension/src-language-server/stpa/diagram/utils.ts +++ b/extension/src-language-server/stpa/diagram/utils.ts @@ -197,7 +197,7 @@ export function setLevelOfCSNodes(nodes: Node[]): void { function assignLevel(node: Node, visited: Map>): void { for (const action of node.actions) { const target = action.target.ref; - if (getCommonAncestor(node, target!) === node.$container && target && !visited.get(node.name)?.has(target.name)) { + if (target && node.$container === target.$container && target && !visited.get(node.name)?.has(target.name)) { visited.get(node.name)?.add(target.name); if (target.level === undefined || target.level < node.level! + 1) { target.level = node.level! + 1; @@ -207,7 +207,7 @@ function assignLevel(node: Node, visited: Map>): void { } for (const feedback of node.feedbacks) { const target = feedback.target.ref; - if (getCommonAncestor(node, target!) === node.$container && target && !visited.get(node.name)?.has(target.name)) { + if (target && node.$container === target.$container && target && !visited.get(node.name)?.has(target.name)) { visited.get(node.name)?.add(target.name); if (target.level === undefined || target.level > node.level! - 1) { target.level = node.level! - 1; diff --git a/extension/src-webview/stpa/stpa-views.tsx b/extension/src-webview/stpa/stpa-views.tsx index dc92cac0..563c5938 100644 --- a/extension/src-webview/stpa/stpa-views.tsx +++ b/extension/src-webview/stpa/stpa-views.tsx @@ -23,7 +23,7 @@ import { DISymbol } from '../di.symbols'; import { ColorStyleOption, DifferentFormsOption, RenderOptionsRegistry } from '../options/render-options-registry'; import { renderDiamond, renderHexagon, renderMirroredTriangle, renderOval, renderPentagon, renderRectangle, renderRoundedRectangle, renderTrapez, renderTriangle } from '../views-rendering'; import { collectAllChildren } from './helper-methods'; -import { CSEdge, CS_EDGE_TYPE, EdgeType, STPAAspect, STPAEdge, STPANode, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE } from './stpa-model'; +import { CSEdge, CS_EDGE_TYPE, CS_INTERMEDIATE_EDGE_TYPE, EdgeType, STPAAspect, STPAEdge, STPANode, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE } from './stpa-model'; /** Determines if path/aspect highlighting is currently on. */ let highlighting: boolean; @@ -44,7 +44,7 @@ export class PolylineArrowEdgeView extends PolylineEdgeView { // if an STPANode is selected, the components not connected to it should fade out const hidden = (edge.type === STPA_EDGE_TYPE || edge.type === STPA_INTERMEDIATE_EDGE_TYPE) && highlighting && !(edge as STPAEdge).highlight; // feedback edges in the control structure should be dashed - const feedbackEdge = edge.type === CS_EDGE_TYPE && (edge as CSEdge).edgeType === EdgeType.FEEDBACK; + const feedbackEdge = (edge.type === CS_EDGE_TYPE || edge.type === CS_INTERMEDIATE_EDGE_TYPE) && (edge as CSEdge).edgeType === EdgeType.FEEDBACK; const colorStyle = this.renderOptionsRegistry.getValue(ColorStyleOption); const printEdge = colorStyle === "black & white"; From 6a47c74d67aee98da87d5589580162db87ce2870 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 8 Feb 2024 13:49:59 +0100 Subject: [PATCH 07/16] fixed port sides of invisible node ports --- .../stpa/diagram/diagram-generator.ts | 14 +++++++------- .../stpa/diagram/layout-config.ts | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 5a3be9bc..1c657cb5 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -469,11 +469,11 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // add ports for source and target const sourceNode = this.idToSNode.get(sourceId); const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); - sourceNode?.children?.push(this.createSTPAPort(sourcePortId, sourceSide)); + sourceNode?.children?.push(this.createPort(sourcePortId, sourceSide)); const targetNode = this.idToSNode.get(targetId!); const targetPortId = idCache.uniqueId(edgeId + "_newTransition"); - targetNode?.children?.push(this.createSTPAPort(targetPortId, targetSide)); + targetNode?.children?.push(this.createPort(targetPortId, targetSide)); return { sourcePortId, targetPortId }; } @@ -764,7 +764,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // add port for source node const sourceNode = this.idToSNode.get(sourceId); const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); - sourceNode?.children?.push(this.createSTPAPort(sourcePortId, PortSide.NORTH)); + sourceNode?.children?.push(this.createPort(sourcePortId, PortSide.NORTH)); // add edge from source to top parent of the target return this.createSTPAEdge( @@ -843,7 +843,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { const currentId = idCache.getId(current); const currentNode = this.idToSNode.get(currentId!); const portId = idCache.uniqueId(edgeId + "_newTransition"); - currentNode?.children?.push(this.createSTPAPort(portId, side)); + currentNode?.children?.push(this.createPort(portId, side)); ids.push(portId); current = current?.$container; } @@ -913,12 +913,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.createSTPAPort(invisiblePortId, side)); + invisibleChild.children?.push(this.createPort(invisiblePortId, side)); ids.push(invisiblePortId); nodes.push(invisibleChild); } const nodePortId = idCache.uniqueId(edgeId + "_newTransition"); - currentNode?.children?.push(this.createSTPAPort(nodePortId, side)); + currentNode?.children?.push(this.createPort(nodePortId, side)); ids.push(nodePortId); nodes.push(currentNode); current = current?.$container; @@ -967,7 +967,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param side The side of the port. * @returns an STPAPort. */ - protected createSTPAPort(id: string, side: PortSide): STPAPort { + protected createPort(id: string, side: PortSide): STPAPort { return { type: STPA_PORT_TYPE, id: id, diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index 43248778..26db80a9 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -88,6 +88,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { return { "org.eclipse.elk.partitioning.activate": "true", "org.eclipse.elk.direction": "DOWN", + "org.eclipse.elk.portConstraints": "FIXED_SIDE", }; } From c61bb40abf7cf6547040e5a3b9596b74964c5cb6 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 8 Feb 2024 15:34:16 +0100 Subject: [PATCH 08/16] fixed order of cs children (nodes and edges) --- .../stpa/diagram/diagram-generator.ts | 23 +++++++++++++---- .../stpa/diagram/layout-config.ts | 25 ++++++++++++++++--- .../stpa/diagram/stpa-model.ts | 1 + extension/src-webview/di.config.ts | 2 ++ extension/src-webview/stpa/stpa-model.ts | 1 + 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 1c657cb5..0c091e49 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -47,6 +47,7 @@ import { HEADER_LABEL_TYPE, INVISIBLE_NODE_TYPE, PARENT_TYPE, + PROCESS_MODEL_NODE_TYPE, PortSide, STPAAspect, STPA_EDGE_TYPE, @@ -305,7 +306,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { const nodeId = idCache.uniqueId(node.name, node); const children: SModelElement[] = this.createLabel([label], nodeId, idCache); if (this.options.getShowProcessModels()) { - children.push(...this.createProcessModelNodes(node.variables, idCache)); + children.push(this.createProcessModelNodes(node.variables, idCache)); } // add children of the control structure node if (node.children?.length !== 0) { @@ -343,7 +344,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { return csNode; } - protected createProcessModelNodes(variables: Variable[], idCache: IdCache): SModelElement[] { + protected createProcessModelNodes(variables: Variable[], idCache: IdCache): SModelElement { const csChildren: SModelElement[] = []; for (const variable of variables) { const label = variable.name; @@ -367,7 +368,19 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { } as CSNode; csChildren.push(csNode); } - return csChildren; + const invisibleNode = { + type: PROCESS_MODEL_NODE_TYPE, + id: idCache.uniqueId("invisible"), + children: csChildren, + layout: "stack", + layoutOptions: { + paddingTop: 10.0, + paddingBottom: 10.0, + paddingLeft: 10.0, + paddingRight: 10.0, + }, + }; + return invisibleNode; } /** @@ -380,8 +393,6 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { const edges: (CSNode | CSEdge)[] = []; // for every control action and feedback of every a node, a edge should be created for (const node of nodes) { - // create edges for children and add the ones that must be added at the top level - edges.push(...this.generateVerticalCSEdges(node.children, args)); // create edges representing the control actions edges.push(...this.translateCommandsToEdges(node.actions, EdgeType.CONTROL_ACTION, args)); // create edges representing feedback @@ -390,6 +401,8 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { edges.push(...this.translateIOToEdgeAndNode(node.inputs, node, EdgeType.INPUT, args)); // create edges representing the other outputs edges.push(...this.translateIOToEdgeAndNode(node.outputs, node, EdgeType.OUTPUT, args)); + // create edges for children and add the ones that must be added at the top level + edges.push(...this.generateVerticalCSEdges(node.children, args)); } return edges; } diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index 26db80a9..fbf88913 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -20,7 +20,15 @@ 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 { CS_NODE_TYPE, INVISIBLE_NODE_TYPE, PARENT_TYPE, PortSide, STPA_NODE_TYPE, STPA_PORT_TYPE } from "./stpa-model"; +import { + CS_NODE_TYPE, + INVISIBLE_NODE_TYPE, + PARENT_TYPE, + PROCESS_MODEL_NODE_TYPE, + PortSide, + STPA_NODE_TYPE, + STPA_PORT_TYPE, +} from "./stpa-model"; export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { protected graphOptions(sgraph: SGraph, index: SModelIndex): LayoutOptions { @@ -77,18 +85,30 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { return this.csNodeOptions(snode as CSNode); case INVISIBLE_NODE_TYPE: return this.invisibleNodeOptions(snode); + case PROCESS_MODEL_NODE_TYPE: + return this.processModelNodeOptions(snode); case STPA_NODE_TYPE: return this.stpaNodeOptions(snode as STPANode); case PARENT_TYPE: return this.grandparentNodeOptions(snode as ParentNode, index); } } + 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" + }; + } protected invisibleNodeOptions(snode: SNode): LayoutOptions | undefined { return { "org.eclipse.elk.partitioning.activate": "true", "org.eclipse.elk.direction": "DOWN", "org.eclipse.elk.portConstraints": "FIXED_SIDE", + "org.eclipse.elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES", + "org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true" }; } @@ -140,10 +160,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { if (node.children?.find(child => child.type.startsWith("node"))) { // cs nodes with children options["org.eclipse.elk.nodeLabels.placement"] = "INSIDE V_TOP H_CENTER"; - // TODO: want to use rectpacking instead of layered but this does not work at the moment (sprotty issue) - // options['org.eclipse.elk.algorithm'] = 'RECTPACKING'; options["org.eclipse.elk.direction"] = "DOWN"; - options["org.eclipse.elk.separateConnectedComponents"] = "false"; options["org.eclipse.elk.partitioning.activate"] = "true"; } else { // TODO: want H_LEFT but this expands the node more than needed diff --git a/extension/src-language-server/stpa/diagram/stpa-model.ts b/extension/src-language-server/stpa/diagram/stpa-model.ts index 9f91b003..95a8f9e8 100644 --- a/extension/src-language-server/stpa/diagram/stpa-model.ts +++ b/extension/src-language-server/stpa/diagram/stpa-model.ts @@ -20,6 +20,7 @@ export const STPA_NODE_TYPE = 'node:stpa'; export const PARENT_TYPE= 'node:parent'; export const CS_NODE_TYPE = 'node:cs'; export const INVISIBLE_NODE_TYPE = 'node:invisible'; +export const PROCESS_MODEL_NODE_TYPE = 'node:processModel'; export const DUMMY_NODE_TYPE = 'node:dummy'; export const EDGE_TYPE = 'edge'; export const CS_EDGE_TYPE = 'edge:controlStructure'; diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index a2e4a989..9f30f576 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -59,6 +59,7 @@ import { HEADER_LABEL_TYPE, INVISIBLE_NODE_TYPE, PARENT_TYPE, + PROCESS_MODEL_NODE_TYPE, STPAEdge, STPANode, STPAPort, @@ -108,6 +109,7 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = // STPA configureModelElement(context, "graph", SGraph, STPAGraphView); configureModelElement(context, INVISIBLE_NODE_TYPE, SNode, InvisibleNodeView); + configureModelElement(context, PROCESS_MODEL_NODE_TYPE, SNode, InvisibleNodeView); configureModelElement(context, DUMMY_NODE_TYPE, CSNode, CSNodeView); configureModelElement(context, CS_NODE_TYPE, CSNode, CSNodeView); configureModelElement(context, STPA_NODE_TYPE, STPANode, STPANodeView); diff --git a/extension/src-webview/stpa/stpa-model.ts b/extension/src-webview/stpa/stpa-model.ts index 8203d4e1..e3c20551 100644 --- a/extension/src-webview/stpa/stpa-model.ts +++ b/extension/src-webview/stpa/stpa-model.ts @@ -22,6 +22,7 @@ export const STPA_NODE_TYPE = 'node:stpa'; export const PARENT_TYPE = 'node:parent'; export const CS_NODE_TYPE = 'node:cs'; export const INVISIBLE_NODE_TYPE = 'node:invisible'; +export const PROCESS_MODEL_NODE_TYPE = 'node:processModel'; export const DUMMY_NODE_TYPE = 'node:dummy'; export const EDGE_TYPE = 'edge'; export const CS_EDGE_TYPE = 'edge:controlStructure'; From a81b01c99aede4ee9f65e04a2a551c1465197d33 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 9 Feb 2024 15:54:40 +0100 Subject: [PATCH 09/16] adjusted padding of cs nodes with children --- .../fta/diagram/fta-diagram-generator.ts | 2 +- .../stpa/diagram/diagram-generator.ts | 16 ++++++++++------ .../stpa/diagram/layout-config.ts | 2 ++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index 09d2ac00..59ecbbd3 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -266,7 +266,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { layoutOptions: { paddingTop: 0.0, paddingBottom: 10.0, - paddngLeft: 0.0, + paddingLeft: 0.0, paddingRight: 0.0, }, }; diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 0c091e49..ef820ca5 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -883,7 +883,8 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { [], edgeType, sEdgeType, - args + args, + false ) ); } @@ -897,7 +898,8 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { [], edgeType, sEdgeType, - args + args, + false ) ); } @@ -1033,7 +1035,8 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { label: string[], edgeType: EdgeType, sedgeType: string, - args: GeneratorContext + args: GeneratorContext, + dummyLabel: boolean = true ): CSEdge { return { type: sedgeType, @@ -1041,7 +1044,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { sourceId: sourceId!, targetId: targetId!, edgeType: edgeType, - children: this.createLabel(label, edgeId, args.idCache), + children: this.createLabel(label, edgeId, args.idCache, undefined, dummyLabel), }; } @@ -1055,7 +1058,8 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { label: string[], id: string, idCache: IdCache, - type: string = "label:xref" + type: string = "label:xref", + dummyLabel: boolean = true ): SLabel[] { const children: SLabel[] = []; if (label.find(l => l !== "")) { @@ -1066,7 +1070,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { text: l, } as SLabel); }); - } else { + } else if (dummyLabel) { // needed for correct layout children.push({ type: type, diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index fbf88913..dbbd80d8 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -162,6 +162,8 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { options["org.eclipse.elk.nodeLabels.placement"] = "INSIDE V_TOP H_CENTER"; options["org.eclipse.elk.direction"] = "DOWN"; options["org.eclipse.elk.partitioning.activate"] = "true"; + options["org.eclipse.elk.padding"] = "[top=0.0,left=0.0,bottom=0.0,right=0.0]"; + options["org.eclipse.elk.spacing.portPort"] = "0.0"; } else { // TODO: want H_LEFT but this expands the node more than needed options["org.eclipse.elk.nodeLabels.placement"] = "INSIDE V_CENTER H_CENTER"; From 5da476ee64168edfb811d2f5960cd6153cd68814 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 13 Feb 2024 10:57:03 +0100 Subject: [PATCH 10/16] 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} } /** From 5d2573c512b6f9ad3ecad6c7e5b00fab0cdf14e2 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 15 Feb 2024 10:35:18 +0100 Subject: [PATCH 11/16] fixed linter issues --- .../stpa/diagram/diagram-generator.ts | 2 +- .../src-language-server/stpa/diagram/utils.ts | 11 ++- .../stpa/stpa-scopeProvider.ts | 76 ++++++++++++++++--- extension/src-webview/stpa/stpa-model.ts | 16 ++-- 4 files changed, 79 insertions(+), 26 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index c86c7783..e34e9c87 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -906,7 +906,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ); } for (let i = 0; i < targets.nodes.length - 1; i++) { - const sEdgeType = i == 0 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE; + const sEdgeType = i === 0 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE; targets.nodes[i + 1]?.children?.push( this.createControlStructureEdge( args.idCache.uniqueId(edgeId), diff --git a/extension/src-language-server/stpa/diagram/utils.ts b/extension/src-language-server/stpa/diagram/utils.ts index 571a43ed..40c9df6b 100644 --- a/extension/src-language-server/stpa/diagram/utils.ts +++ b/extension/src-language-server/stpa/diagram/utils.ts @@ -37,7 +37,7 @@ import { import { CSNode, PastaPort, STPANode } from "./stpa-interfaces"; import { STPAAspect } from "./stpa-model"; import { groupValue } from "./stpa-synthesis-options"; -import { SModelElement } from "sprotty-protocol" +import { SModelElement } from "sprotty-protocol"; /** * Getter for the references contained in {@code node}. @@ -390,8 +390,8 @@ 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[] = [] + const ports: PastaPort[] = []; + const otherChildren: SModelElement[] = []; node.children?.forEach(child => { if (child.type.startsWith("port")) { ports.push(child as any as PastaPort); @@ -405,7 +405,10 @@ export function sortPorts(nodes: CSNode[]): void { newPorts.push(port); if (port.assocEdge) { for (const otherPort of ports) { - if (port.assocEdge.node1 == otherPort.assocEdge?.node2 && port.assocEdge.node2 == otherPort.assocEdge.node1) { + if ( + port.assocEdge.node1 === otherPort.assocEdge?.node2 && + port.assocEdge.node2 === otherPort.assocEdge.node1 + ) { newPorts.push(otherPort); ports.splice(ports.indexOf(otherPort), 1); } diff --git a/extension/src-language-server/stpa/stpa-scopeProvider.ts b/extension/src-language-server/stpa/stpa-scopeProvider.ts index 9e761c9e..0914cc9b 100644 --- a/extension/src-language-server/stpa/stpa-scopeProvider.ts +++ b/extension/src-language-server/stpa/stpa-scopeProvider.ts @@ -25,14 +25,44 @@ import { Scope, Stream, getDocument, - stream + stream, } from "langium"; -import { ActionUCAs, Command, Context, DCAContext, DCARule, Hazard, LossScenario, Model, Node, Rule, SystemConstraint, UCA, Variable, isActionUCAs, isControllerConstraint, isContext, isDCAContext, isDCARule, isHazardList, isLossScenario, isModel, isResponsibility, isSystemResponsibilities, isRule, isSafetyConstraint, isSystemConstraint, isNode, isVerticalEdge, VerticalEdge, isGraph, Graph } from "../generated/ast"; +import { + ActionUCAs, + Command, + Context, + DCAContext, + DCARule, + Hazard, + LossScenario, + Model, + Node, + Rule, + SystemConstraint, + UCA, + Variable, + isActionUCAs, + isControllerConstraint, + isContext, + isDCAContext, + isDCARule, + isHazardList, + isLossScenario, + isModel, + isResponsibility, + isSystemResponsibilities, + isRule, + isSafetyConstraint, + isSystemConstraint, + isNode, + isVerticalEdge, + VerticalEdge, + isGraph, + Graph, +} from "../generated/ast"; import { StpaServices } from "./stpa-module"; - export class StpaScopeProvider extends DefaultScopeProvider { - /* the types of the different aspects */ private CA_TYPE = Command; private HAZARD_TYPE = Hazard; @@ -57,9 +87,17 @@ export class StpaScopeProvider extends DefaultScopeProvider { } if (precomputed && model) { // determine the scope for the different aspects & reference types - if ((isControllerConstraint(node) || isLossScenario(node) || isSafetyConstraint(node)) && (referenceType === this.UCA_TYPE || referenceType === this.CONTEXT_TYPE)) { + if ( + (isControllerConstraint(node) || isLossScenario(node) || isSafetyConstraint(node)) && + (referenceType === this.UCA_TYPE || referenceType === this.CONTEXT_TYPE) + ) { return this.getUCAs(model, precomputed); - } else if (isHazardList(node) && isLossScenario(node.$container) && node.$container.uca && referenceType === this.HAZARD_TYPE) { + } else if ( + isHazardList(node) && + isLossScenario(node.$container) && + node.$container.uca && + referenceType === this.HAZARD_TYPE + ) { return this.getUCAHazards(node.$container, model, precomputed); } else if (isResponsibility(node) && referenceType === this.SYS_CONSTRAINT_TYPE) { return this.getSystemConstraints(model, precomputed); @@ -89,7 +127,10 @@ export class StpaScopeProvider extends DefaultScopeProvider { private getStandardScope(node: AstNode, referenceType: string, precomputed: PrecomputedScopes): Scope { let currentNode: AstNode | undefined = node; // responsibilities, UCAs, and DCAs should have references to the nodes in the control structure - if ((isSystemResponsibilities(node) || isActionUCAs(node) || isRule(node) || isDCARule(node)) && referenceType === Node) { + if ( + (isSystemResponsibilities(node) || isActionUCAs(node) || isRule(node) || isDCARule(node)) && + referenceType === Node + ) { const model = node.$container as Model; currentNode = model.controlStructure; } @@ -137,7 +178,7 @@ export class StpaScopeProvider extends DefaultScopeProvider { while (graph && !isGraph(graph)) { graph = graph.$container; } - + const allDescriptions = this.getChildrenNodes(graph.nodes, precomputed); return this.descriptionsToScope(allDescriptions); } @@ -192,7 +233,11 @@ export class StpaScopeProvider extends DefaultScopeProvider { * @returns Scope containing all system-level constraints. */ private getSystemConstraints(model: Model, precomputed: PrecomputedScopes): Scope { - const allDescriptions = this.getHazardSysCompsDescriptions(model.systemLevelConstraints, precomputed, this.SYS_CONSTRAINT_TYPE); + const allDescriptions = this.getHazardSysCompsDescriptions( + model.systemLevelConstraints, + precomputed, + this.SYS_CONSTRAINT_TYPE + ); return this.descriptionsToScope(allDescriptions); } @@ -203,7 +248,11 @@ export class StpaScopeProvider extends DefaultScopeProvider { * @param type Type of the seacrhed aspect. Either hazard or system constraint. * @returns All defnitions of hazards or constraints depending on {@code type}. */ - private getHazardSysCompsDescriptions(nodes: (Hazard | SystemConstraint)[], precomputed: PrecomputedScopes, type: string): AstNodeDescription[] { + private getHazardSysCompsDescriptions( + nodes: (Hazard | SystemConstraint)[], + precomputed: PrecomputedScopes, + type: string + ): AstNodeDescription[] { if (type === this.HAZARD_TYPE || type === this.SYS_CONSTRAINT_TYPE) { let res: AstNodeDescription[] = []; for (const node of nodes) { @@ -247,7 +296,11 @@ export class StpaScopeProvider extends DefaultScopeProvider { * @param precomputed Precomputed Scope of the document. * @returns Descriptions of type {@code type} for {@code currentNode}. */ - private getDescriptions(currentNode: AstNode | undefined, type: string, precomputed: PrecomputedScopes): AstNodeDescription[] { + private getDescriptions( + currentNode: AstNode | undefined, + type: string, + precomputed: PrecomputedScopes + ): AstNodeDescription[] { let res: AstNodeDescription[] = []; while (currentNode) { const allDescriptions = precomputed.get(currentNode); @@ -273,5 +326,4 @@ export class StpaScopeProvider extends DefaultScopeProvider { } return result; } - } diff --git a/extension/src-webview/stpa/stpa-model.ts b/extension/src-webview/stpa/stpa-model.ts index d1cb02d4..7954dd19 100644 --- a/extension/src-webview/stpa/stpa-model.ts +++ b/extension/src-webview/stpa/stpa-model.ts @@ -40,8 +40,7 @@ export class ParentNode extends SNode { * Node representing an STPA component. */ export class STPANode extends SNode { - static readonly DEFAULT_FEATURES = [connectableFeature, selectFeature, - layoutContainerFeature, fadeFeature]; + static readonly DEFAULT_FEATURES = [connectableFeature, selectFeature, layoutContainerFeature, fadeFeature]; aspect: STPAAspect = STPAAspect.UNDEFINED; description: string = ""; @@ -64,7 +63,7 @@ export class STPAEdge extends SEdge { /** Port representing a port in the STPA graph. */ export class PastaPort extends SPort { side?: PortSide; - assocEdge?: {node1: string, node2: string} + assocEdge?: { node1: string; node2: string }; } /** @@ -73,8 +72,7 @@ export class PastaPort extends SPort { export class CSNode extends SNode { level?: number; // processmodel? - static readonly DEFAULT_FEATURES = [connectableFeature, selectFeature, - layoutContainerFeature, fadeFeature]; + static readonly DEFAULT_FEATURES = [connectableFeature, selectFeature, layoutContainerFeature, fadeFeature]; } /** @@ -97,7 +95,7 @@ export enum STPAAspect { CONTROLLERCONSTRAINT, SCENARIO, SAFETYREQUIREMENT, - UNDEFINED + UNDEFINED, } /** @@ -108,7 +106,7 @@ export enum EdgeType { FEEDBACK, INPUT, OUTPUT, - UNDEFINED + UNDEFINED, } /** Possible sides for a port. */ @@ -116,5 +114,5 @@ export enum PortSide { WEST, EAST, NORTH, - SOUTH -} \ No newline at end of file + SOUTH, +} From c8eb27a455ae0e2b0fa6e6be629fadb27fcbb111 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 15 Feb 2024 15:49:04 +0100 Subject: [PATCH 12/16] added comments --- .../stpa/diagram/diagram-generator.ts | 84 +++++++++++++++---- .../stpa/diagram/layout-config.ts | 42 +++++++--- .../stpa/diagram/stpa-interfaces.ts | 32 +++---- .../stpa/diagram/stpa-model.ts | 8 +- .../stpa/diagram/stpa-synthesis-options.ts | 12 +-- .../src-language-server/stpa/diagram/utils.ts | 41 ++++++--- .../stpa/stpa-scopeProvider.ts | 27 ++++-- extension/src-webview/di.config.ts | 40 ++++++--- extension/src-webview/stpa/stpa-model.ts | 10 +-- 9 files changed, 208 insertions(+), 88 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index e34e9c87..2204b423 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2021-2023 by + * Copyright 2021-2024 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -37,23 +37,23 @@ import { getDescription } from "../../utils"; import { StpaServices } from "../stpa-module"; import { collectElementsWithSubComps, leafElement } from "../utils"; import { filterModel } from "./filtering"; -import { CSEdge, CSNode, ParentNode, STPAEdge, STPANode, PastaPort } from "./stpa-interfaces"; +import { CSEdge, CSNode, ParentNode, PastaPort, STPAEdge, STPANode } from "./stpa-interfaces"; import { CS_EDGE_TYPE, CS_INTERMEDIATE_EDGE_TYPE, + CS_INVISIBLE_SUBCOMPONENT_TYPE, CS_NODE_TYPE, DUMMY_NODE_TYPE, EdgeType, HEADER_LABEL_TYPE, - INVISIBLE_NODE_TYPE, PARENT_TYPE, - PROCESS_MODEL_NODE_TYPE, + PORT_TYPE, + PROCESS_MODEL_PARENT_NODE_TYPE, PortSide, STPAAspect, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE, STPA_NODE_TYPE, - PORT_TYPE, } from "./stpa-model"; import { StpaSynthesisOptions, showLabelsValue } from "./stpa-synthesis-options"; import { @@ -272,6 +272,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ...this.generateVerticalCSEdges(filteredModel.controlStructure.nodes, args), //...this.generateHorizontalCSEdges(filteredModel.controlStructure.edges, args) ]; + // sort the ports in order to group edges based on the nodes they are connected to sortPorts(CSChildren.filter(node => node.type.startsWith("node")) as CSNode[]); // add control structure to roots children rootChildren.push({ @@ -308,12 +309,14 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { const nodeId = idCache.uniqueId(node.name, node); const children: SModelElement[] = this.createLabel([label], nodeId, idCache); if (this.options.getShowProcessModels()) { + // add nodes representing the process model children.push(this.createProcessModelNodes(node.variables, idCache)); } // add children of the control structure node if (node.children?.length !== 0) { + // add invisible node to group the children in order to be able to lay them out separately from the process model node const invisibleNode = { - type: INVISIBLE_NODE_TYPE, + type: CS_INVISIBLE_SUBCOMPONENT_TYPE, id: idCache.uniqueId(node.name + "_invisible"), children: [] as SModelElement[], layout: "stack", @@ -324,6 +327,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { paddingRight: 10.0, }, }; + // create the actual children node.children?.forEach(child => { invisibleNode.children?.push(this.createControlStructureNode(child, args)); }); @@ -346,9 +350,16 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { return csNode; } - protected createProcessModelNodes(variables: Variable[], idCache: IdCache): SModelElement { + /** + * Creates nodes representing the process model defined by the {@code variables} and encapsulates them in an invisible node. + * @param variables The variables of the process model. + * @param idCache The id cache of the STPA model. + * @returns an invisible node containing the nodes representing the process model. + */ + protected createProcessModelNodes(variables: Variable[], idCache: IdCache): SNode { const csChildren: SModelElement[] = []; for (const variable of variables) { + // translate the variable name to a header label and the values to further labels const label = variable.name; const nodeId = idCache.uniqueId(variable.name, variable); const values = variable.values?.map(value => value.name); @@ -356,6 +367,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ...this.createLabel([label], nodeId, idCache, HEADER_LABEL_TYPE), ...this.createLabel(values, nodeId, idCache), ]; + // create the actual node with the created labels const csNode = { type: CS_NODE_TYPE, id: nodeId, @@ -370,8 +382,9 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { } as CSNode; csChildren.push(csNode); } + // encapsulate the nodes representing the process model in an invisible node const invisibleNode = { - type: PROCESS_MODEL_NODE_TYPE, + type: PROCESS_MODEL_PARENT_NODE_TYPE, id: idCache.uniqueId("invisible"), children: csChildren, layout: "stack", @@ -399,6 +412,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { edges.push(...this.translateCommandsToEdges(node.actions, EdgeType.CONTROL_ACTION, args)); // create edges representing feedback edges.push(...this.translateCommandsToEdges(node.feedbacks, EdgeType.FEEDBACK, args)); + // FIXME: input/output does not work anymore // create edges representing the other inputs edges.push(...this.translateIOToEdgeAndNode(node.inputs, node, EdgeType.INPUT, args)); // create edges representing the other outputs @@ -410,11 +424,11 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { } /** - * Translates the commands (control action or feedback) of a node to edges. + * Translates the commands (control action or feedback) of a node to (intermediate) edges and adds them to the correct nodes. * @param commands The control actions or feedback of a node. * @param edgeType The type of the edge (control action or feedback). * @param args GeneratorContext of the STPA model. - * @returns A list of edges representing the commands. + * @returns A list of edges representing the commands that should be added at the top level. */ protected translateCommandsToEdges( commands: VerticalEdge[], @@ -424,6 +438,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { const idCache = args.idCache; const edges: CSEdge[] = []; for (const edge of commands) { + // create edge id const source = edge.$container; const target = edge.target.ref; const edgeId = idCache.uniqueId( @@ -432,15 +447,15 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ); if (target) { - // multiple commands to same target is represented by one edge + // multiple commands to same target is represented by one edge -> combine labels to one const label: string[] = []; for (let i = 0; i < edge.comms.length; i++) { const com = edge.comms[i]; label.push(com.label); } - // edges can be hierachy crossing so we must determine the common ancestor + // edges can be hierachy crossing so we must determine the common ancestor of source and target const commonAncestor = getCommonAncestor(source, target); - // create the intermediate ports and edges for the control action + // create the intermediate ports and edges const ports = this.generateIntermediateCSEdges(source, target, edgeId, edgeType, args, commonAncestor); // add edge between the two ports in the common ancestor const csEdge = this.createControlStructureEdge( @@ -449,14 +464,19 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ports.targetPort, label, edgeType, + // if the common ancestor is the parent of the target we want an edge with an arrow otherwise an intermediate edge target.$container === commonAncestor ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE, args ); if (commonAncestor?.$type === "Graph") { + // if the common ancestor is the graph, the edge must be added at the top level and hence have to be returned edges.push(csEdge); } else if (commonAncestor) { + // if the common ancestor is a node, the edge must be added to the children of the common ancestor const snodeAncestor = this.idToSNode.get(idCache.getId(commonAncestor)!); - snodeAncestor?.children?.find(node => node.type === INVISIBLE_NODE_TYPE)?.children?.push(csEdge); + snodeAncestor?.children + ?.find(node => node.type === CS_INVISIBLE_SUBCOMPONENT_TYPE) + ?.children?.push(csEdge); } } } @@ -865,6 +885,16 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { return ids; } + /** + * Generates intermediate edges and ports for the given {@code source} and {@code target} to connect them through hierarchical levels. + * @param source The source of the edge. + * @param target The target of the edge. + * @param edgeId The ID of the original edge. + * @param edgeType The type of the edge. + * @param args The GeneratorContext of the STPA model. + * @param ancestor The common ancestor of the source and target. + * @returns the IDs of the source and target port at the hierarchy level of the {@code ancestor}. + */ protected generateIntermediateCSEdges( source: Node | undefined, target: Node | undefined, @@ -874,6 +904,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ancestor?: Node | Graph ): { sourcePort: string; targetPort: string } { const assocEdge = { node1: source?.name ?? "", node2: target?.name ?? "" }; + // add ports for source and target and their ancestors till the common ancestor const sources = this.generatePortsForCSHierarchy( source, assocEdge, @@ -890,6 +921,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { args.idCache, ancestor ); + // add edges between the ports of the source and its ancestors for (let i = 0; i < sources.nodes.length - 1; i++) { const sEdgeType = CS_INTERMEDIATE_EDGE_TYPE; sources.nodes[i + 1]?.children?.push( @@ -905,6 +937,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) ); } + // add edges between the ports of the target and its ancestors for (let i = 0; i < targets.nodes.length - 1; i++) { const sEdgeType = i === 0 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE; targets.nodes[i + 1]?.children?.push( @@ -920,13 +953,23 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) ); } + // return the source and target port at the hierarchy level of the ancestor return { sourcePort: sources.portIds[sources.portIds.length - 1], targetPort: targets.portIds[targets.portIds.length - 1], }; } - // adds ports for current node and its (grand)parents up to the ancestor. The ancestor get no port. + /** + * Adds ports for the {@code current} node and its (grand)parents up to the {@code ancestor}. + * @param current The node for which the ports should be created. + * @param assocEdge The associated edge for which the ports should be created. + * @param edgeId The ID of the original edge for which the ports should be created. + * @param side The side of the ports. + * @param idCache The ID cache of the STPA model. + * @param ancestor The common ancestor of the source and target of the associated edge. + * @returns the IDs of the created ports and the nodes the ports were added to. + */ protected generatePortsForCSHierarchy( current: AstNode | undefined, assocEdge: { node1: string; node2: string }, @@ -942,7 +985,10 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { if (currentId) { const currentNode = this.idToSNode.get(currentId); if (currentNode) { - const invisibleChild = currentNode?.children?.find(child => child.type === INVISIBLE_NODE_TYPE); + // current node could have an invisible child that was skipped while going up the hierarchy because it does not exist in the AST + const invisibleChild = currentNode?.children?.find( + child => child.type === CS_INVISIBLE_SUBCOMPONENT_TYPE + ); if (invisibleChild && ids.length !== 0) { // add port for the invisible node first const invisiblePortId = idCache.uniqueId(edgeId + "_newTransition"); @@ -950,6 +996,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ids.push(invisiblePortId); nodes.push(invisibleChild); } + // add port for the current node const nodePortId = idCache.uniqueId(edgeId + "_newTransition"); currentNode?.children?.push(this.createPort(nodePortId, side, assocEdge)); ids.push(nodePortId); @@ -1005,7 +1052,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { type: PORT_TYPE, id: id, side: side, - assocEdge: assocEdge, + associatedEdge: assocEdge, }; } @@ -1071,6 +1118,9 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * Generates SLabel elements for the given {@code label}. * @param label Labels to translate to SLabel elements. * @param id The ID of the element for which the label should be generated. + * @param idCache The ID cache of the STPA model. + * @param type The type of the label. + * @param dummyLabel Determines whether a dummy label should be created to get a correct layout. * @returns SLabel elements representing {@code label}. */ protected createLabel( diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index 7923f107..aa1f5fd1 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2021-2023 by + * Copyright 2021-2024 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -22,9 +22,9 @@ import { SGraph, SModelIndex, SNode, SPort } from "sprotty-protocol"; import { CSNode, ParentNode, STPANode, PastaPort } from "./stpa-interfaces"; import { CS_NODE_TYPE, - INVISIBLE_NODE_TYPE, + CS_INVISIBLE_SUBCOMPONENT_TYPE, PARENT_TYPE, - PROCESS_MODEL_NODE_TYPE, + PROCESS_MODEL_PARENT_NODE_TYPE, PortSide, STPA_NODE_TYPE, PORT_TYPE, @@ -83,17 +83,21 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { switch (snode.type) { case CS_NODE_TYPE: return this.csNodeOptions(snode as CSNode); - case INVISIBLE_NODE_TYPE: - return this.invisibleNodeOptions(snode); - case PROCESS_MODEL_NODE_TYPE: - return this.processModelNodeOptions(snode); + case CS_INVISIBLE_SUBCOMPONENT_TYPE: + return this.invisibleSubcomponentOptions(snode); + case PROCESS_MODEL_PARENT_NODE_TYPE: + return this.processModelParentNodeOptions(snode); case STPA_NODE_TYPE: return this.stpaNodeOptions(snode as STPANode); case PARENT_TYPE: return this.grandparentNodeOptions(snode as ParentNode, index); } } - processModelNodeOptions(snode: SNode): LayoutOptions | undefined { + + /** + * Options for the invisible node that contains the process model nodes. + */ + processModelParentNodeOptions(snode: SNode): LayoutOptions | undefined { return { "org.eclipse.elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES", "org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true", @@ -102,7 +106,10 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { }; } - protected invisibleNodeOptions(snode: SNode): LayoutOptions | undefined { + /** + * Options for the invisible node that contains subcomponents of a cs node. + */ + protected invisibleSubcomponentOptions(snode: SNode): LayoutOptions | undefined { return { "org.eclipse.elk.partitioning.activate": "true", "org.eclipse.elk.direction": "DOWN", @@ -112,8 +119,12 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { }; } + /** + * Options for the standard STPA nodes. + */ protected stpaNodeOptions(node: STPANode): LayoutOptions { if (node.children?.find(child => child.type.startsWith("node"))) { + // node has further children nodes return this.parentSTPANodeOptions(node); } else { return { @@ -125,6 +136,9 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { } } + /** + * Options for an STPA node that has children nodes. + */ protected parentSTPANodeOptions(node: STPANode): LayoutOptions { // options for nodes in the STPA graphs that have children const options: LayoutOptions = { @@ -149,6 +163,9 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { return options; } + /** + * Options for a standard control structure node. + */ protected csNodeOptions(node: CSNode): LayoutOptions { const options: LayoutOptions = { "org.eclipse.elk.partitioning.partition": "" + node.level, @@ -158,19 +175,22 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { "org.eclipse.elk.portConstraints": "FIXED_SIDE", }; if (node.children?.find(child => child.type.startsWith("node"))) { - // cs nodes with children + // node hast children nodes options["org.eclipse.elk.nodeLabels.placement"] = "INSIDE V_TOP H_CENTER"; options["org.eclipse.elk.direction"] = "DOWN"; options["org.eclipse.elk.partitioning.activate"] = "true"; options["org.eclipse.elk.padding"] = "[top=0.0,left=0.0,bottom=0.0,right=0.0]"; options["org.eclipse.elk.spacing.portPort"] = "0.0"; } else { - // TODO: want H_LEFT but this expands the node more than needed + // TODO: maybe want H_LEFT for process model nodes but this expands the node more than needed options["org.eclipse.elk.nodeLabels.placement"] = "INSIDE V_CENTER H_CENTER"; } return options; } + /** + * Options for a standard port. + */ protected portOptions(sport: SPort, index: SModelIndex): LayoutOptions | undefined { if (sport.type === PORT_TYPE) { let side = ""; diff --git a/extension/src-language-server/stpa/diagram/stpa-interfaces.ts b/extension/src-language-server/stpa/diagram/stpa-interfaces.ts index e3522a50..6408a5d8 100644 --- a/extension/src-language-server/stpa/diagram/stpa-interfaces.ts +++ b/extension/src-language-server/stpa/diagram/stpa-interfaces.ts @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2021-2023 by + * Copyright 2021-2024 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -26,40 +26,40 @@ export interface ParentNode extends SNode { * Node representing a STPA component. */ export interface STPANode extends SNode { - aspect: STPAAspect - description: string - hierarchyLvl: number - highlight?: boolean - level?: number - controlAction?: string - modelOrder?: boolean + aspect: STPAAspect; + description: string; + hierarchyLvl: number; + highlight?: boolean; + level?: number; + controlAction?: string; + modelOrder?: boolean; } /** * Edge representing an edge in the relationship graph. */ - export interface STPAEdge extends SEdge { - aspect: STPAAspect - highlight?: boolean +export interface STPAEdge extends SEdge { + aspect: STPAAspect; + highlight?: boolean; } /** Port representing a port in the STPA graph. */ export interface PastaPort extends SPort { - side?: PortSide - assocEdge?: {node1: string, node2: string} + side?: PortSide; + /** Saves start and end of the edge for which the port was created. Needed to sort the ports based on their associacted edges. */ + associatedEdge?: { node1: string; node2: string }; } /** * Node representing a system component in the control structure. */ export interface CSNode extends SNode { - level?: number - // processmodel? + level?: number; } /** * Edge representing control actions and feedback in the control structure. */ export interface CSEdge extends SEdge { - edgeType: EdgeType + edgeType: EdgeType; } diff --git a/extension/src-language-server/stpa/diagram/stpa-model.ts b/extension/src-language-server/stpa/diagram/stpa-model.ts index f8d6ec42..d5413520 100644 --- a/extension/src-language-server/stpa/diagram/stpa-model.ts +++ b/extension/src-language-server/stpa/diagram/stpa-model.ts @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2021-2023 by + * Copyright 2021-2024 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -19,14 +19,14 @@ export const STPA_NODE_TYPE = 'node:stpa'; export const PARENT_TYPE= 'node:parent'; export const CS_NODE_TYPE = 'node:cs'; -export const INVISIBLE_NODE_TYPE = 'node:invisible'; -export const PROCESS_MODEL_NODE_TYPE = 'node:processModel'; +export const CS_INVISIBLE_SUBCOMPONENT_TYPE = 'node:invisibleSubcomponent'; +export const PROCESS_MODEL_PARENT_NODE_TYPE = 'node:processModelParent'; export const DUMMY_NODE_TYPE = 'node:dummy'; export const EDGE_TYPE = 'edge'; export const CS_EDGE_TYPE = 'edge:controlStructure'; +export const CS_INTERMEDIATE_EDGE_TYPE = 'edge:cs-intermediate'; 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 PORT_TYPE = 'port:pasta'; export const HEADER_LABEL_TYPE = 'label:header'; diff --git a/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts b/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts index 129b4fd9..148af50f 100644 --- a/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts +++ b/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2022-2023 by + * Copyright 2022-2024 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -19,7 +19,7 @@ import { DropDownOption, SynthesisOption, TransformationOptionType, - ValuedSynthesisOption + ValuedSynthesisOption, } from "../../options/option-models"; import { SynthesisOptions, layoutCategory } from "../../synthesis-options"; @@ -80,7 +80,7 @@ const showControlStructureOption: ValuedSynthesisOption = { }; /** - * Boolean option to toggle the visualization of the control structure. + * Boolean option to toggle the visualization of the process model of controllers. */ const showProcessModelsOption: ValuedSynthesisOption = { synthesisOption: { @@ -412,7 +412,7 @@ export class StpaSynthesisOptions extends SynthesisOptions { } setGroupingUCAs(value: groupValue): void { - const option = this.options.find((option) => option.synthesisOption.id === groupingUCAsID); + const option = this.options.find(option => option.synthesisOption.id === groupingUCAsID); if (option) { switch (value) { case groupValue.NO_GROUPING: @@ -443,7 +443,7 @@ export class StpaSynthesisOptions extends SynthesisOptions { } setFilteringUCAs(value: string): void { - const option = this.options.find((option) => option.synthesisOption.id === filteringUCAsID); + const option = this.options.find(option => option.synthesisOption.id === filteringUCAsID); if (option) { option.currentValue = value; option.synthesisOption.currentValue = value; @@ -521,7 +521,7 @@ export class StpaSynthesisOptions extends SynthesisOptions { (option.synthesisOption as DropDownOption).availableValues = values; // if the last selected control action is not available anymore, // set the option to the first control action of the new list - if (!values.find((val) => val.id === (option.synthesisOption as DropDownOption).currentId)) { + if (!values.find(val => val.id === (option.synthesisOption as DropDownOption).currentId)) { (option.synthesisOption as DropDownOption).currentId = values[0].id; option.synthesisOption.currentValue = values[0].id; option.synthesisOption.initialValue = values[0].id; diff --git a/extension/src-language-server/stpa/diagram/utils.ts b/extension/src-language-server/stpa/diagram/utils.ts index 40c9df6b..3e8c3193 100644 --- a/extension/src-language-server/stpa/diagram/utils.ts +++ b/extension/src-language-server/stpa/diagram/utils.ts @@ -16,6 +16,7 @@ */ import { AstNode } from "langium"; +import { SModelElement } from "sprotty-protocol"; import { Context, Graph, @@ -37,7 +38,6 @@ import { 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}. @@ -181,6 +181,7 @@ export function setLevelOfCSNodes(nodes: Node[]): void { for (const node of nodes) { visited.set(node.name, new Set()); if (node.children) { + // set levels of children seperately setLevelOfCSNodes(node.children); } } @@ -191,7 +192,7 @@ export function setLevelOfCSNodes(nodes: Node[]): void { } /** - * Assigns the level to the connected nodes of {@code node}. + * Assigns the level to the connected nodes of {@code node} that are on the same hierarchical level. * @param node The node for which the connected nodes should be assigned a level. * @param visited The edges that have been visited. */ @@ -365,17 +366,28 @@ export function getAspectsThatShouldHaveDesriptions(model: Model): STPAAspect[] return aspectsToShowDescriptions; } -export function getCommonAncestor(node: Node, target: Node): Node | Graph | undefined { - const nodeAncestors = getAncestors(node); - const targetAncestors = getAncestors(target); - for (const ancestor of nodeAncestors) { - if (targetAncestors.includes(ancestor)) { +/** + * Determines the least common ancestor of {@code node1} and {@code node2}. + * @param node1 The first node. + * @param node2 The second node. + * @returns the least common ancestor of {@code node1} and {@code node2} or undefined if none exists. + */ +export function getCommonAncestor(node1: Node, node2: Node): Node | Graph | undefined { + const node1Ancestors = getAncestors(node1); + const node2Ancestors = getAncestors(node2); + for (const ancestor of node1Ancestors) { + if (node2Ancestors.includes(ancestor)) { return ancestor; } } return undefined; } +/** + * Calculates the ancestors of {@code node}. + * @param node The node for which the ancestors should be calculated. + * @returns the ancestors of {@code node}. + */ export function getAncestors(node: Node): (Node | Graph)[] { const ancestors: (Node | Graph)[] = []; let current: Node | Graph | undefined = node; @@ -386,10 +398,17 @@ export function getAncestors(node: Node): (Node | Graph)[] { return ancestors; } +/** + * Sorts the ports of the nodes in {@code nodes} based on their associated edges. + * @param nodes The nodes which ports should be sorted. + */ export function sortPorts(nodes: CSNode[]): void { for (const node of nodes) { + // sort the ports of the children const children = node.children?.filter(child => child.type.startsWith("node")) as CSNode[]; sortPorts(children); + + // separate the ports from the other children const ports: PastaPort[] = []; const otherChildren: SModelElement[] = []; node.children?.forEach(child => { @@ -400,15 +419,17 @@ export function sortPorts(nodes: CSNode[]): void { } }); + // sort the ports based on their associated edges const newPorts: PastaPort[] = []; for (const port of ports) { newPorts.push(port); - if (port.assocEdge) { + if (port.associatedEdge) { for (const otherPort of ports) { if ( - port.assocEdge.node1 === otherPort.assocEdge?.node2 && - port.assocEdge.node2 === otherPort.assocEdge.node1 + port.associatedEdge.node1 === otherPort.associatedEdge?.node2 && + port.associatedEdge.node2 === otherPort.associatedEdge.node1 ) { + // associated edges connect the same nodes but in the opposite direction -> add the other port to the list to group them together newPorts.push(otherPort); ports.splice(ports.indexOf(otherPort), 1); } diff --git a/extension/src-language-server/stpa/stpa-scopeProvider.ts b/extension/src-language-server/stpa/stpa-scopeProvider.ts index 0914cc9b..c96a8af4 100644 --- a/extension/src-language-server/stpa/stpa-scopeProvider.ts +++ b/extension/src-language-server/stpa/stpa-scopeProvider.ts @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2021-2023 by + * Copyright 2021-2024 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -33,6 +33,7 @@ import { Context, DCAContext, DCARule, + Graph, Hazard, LossScenario, Model, @@ -41,24 +42,22 @@ import { SystemConstraint, UCA, Variable, + VerticalEdge, isActionUCAs, - isControllerConstraint, isContext, + isControllerConstraint, isDCAContext, isDCARule, + isGraph, isHazardList, isLossScenario, isModel, isResponsibility, - isSystemResponsibilities, isRule, isSafetyConstraint, isSystemConstraint, - isNode, + isSystemResponsibilities, isVerticalEdge, - VerticalEdge, - isGraph, - Graph, } from "../generated/ast"; import { StpaServices } from "./stpa-module"; @@ -173,6 +172,12 @@ export class StpaScopeProvider extends DefaultScopeProvider { return this.descriptionsToScope(allDescriptions); } + /** + * Creates scope containing all nodes of the control structure. + * @param node Current VerticalEdge. + * @param precomputed Precomputed Scope of the document. + * @returns scope containing all nodes of the control structure. + */ protected getNodes(node: VerticalEdge, precomputed: PrecomputedScopes): Scope { let graph: Node | Graph = node.$container; while (graph && !isGraph(graph)) { @@ -183,7 +188,13 @@ export class StpaScopeProvider extends DefaultScopeProvider { return this.descriptionsToScope(allDescriptions); } - getChildrenNodes(nodes: Node[], precomputed: PrecomputedScopes): AstNodeDescription[] { + /** + * Collects the descriptions of all {@code nodes} and their children. + * @param nodes The nodes for which the descriptions should be collected. + * @param precomputed Precomputed Scope of the document. + * @returns the descriptions of all {@code nodes} and their children. + */ + protected getChildrenNodes(nodes: Node[], precomputed: PrecomputedScopes): AstNodeDescription[] { let res: AstNodeDescription[] = []; for (const node of nodes) { const currentNode: AstNode | undefined = node; diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index b397cbcf..1135133e 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -36,7 +36,7 @@ import { configureModelElement, contextMenuModule, loadDefaultModules, - overrideViewerOptions + overrideViewerOptions, } from "sprotty"; import { SvgCommand } from "./actions"; import { ContextMenuProvider } from "./context-menu/context-menu-provider"; @@ -44,8 +44,26 @@ import { ContextMenuService } from "./context-menu/context-menu-services"; import pastaContextMenuModule from "./context-menu/di.config"; import { SvgPostprocessor } from "./exportPostProcessor"; import { CustomSvgExporter } from "./exporter"; -import { DescriptionNode, FTAEdge, FTAGraph, FTANode, FTAPort, FTA_DESCRIPTION_NODE_TYPE, FTA_EDGE_TYPE, FTA_GRAPH_TYPE, FTA_INVISIBLE_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE } from "./fta/fta-model"; -import { DescriptionNodeView, FTAGraphView, FTAInvisibleEdgeView, FTANodeView, PolylineArrowEdgeViewFTA } from "./fta/fta-views"; +import { + DescriptionNode, + FTAEdge, + FTAGraph, + FTANode, + FTAPort, + FTA_DESCRIPTION_NODE_TYPE, + FTA_EDGE_TYPE, + FTA_GRAPH_TYPE, + FTA_INVISIBLE_EDGE_TYPE, + FTA_NODE_TYPE, + FTA_PORT_TYPE, +} from "./fta/fta-model"; +import { + DescriptionNodeView, + FTAGraphView, + FTAInvisibleEdgeView, + FTANodeView, + PolylineArrowEdgeViewFTA, +} from "./fta/fta-views"; import { PastaModelViewer } from "./model-viewer"; import { optionsModule } from "./options/options-module"; import { sidebarModule } from "./sidebar"; @@ -54,19 +72,19 @@ import { CSNode, CS_EDGE_TYPE, CS_INTERMEDIATE_EDGE_TYPE, + CS_INVISIBLE_SUBCOMPONENT_TYPE, CS_NODE_TYPE, DUMMY_NODE_TYPE, HEADER_LABEL_TYPE, - INVISIBLE_NODE_TYPE, PARENT_TYPE, - PROCESS_MODEL_NODE_TYPE, + PORT_TYPE, + PROCESS_MODEL_PARENT_NODE_TYPE, + PastaPort, STPAEdge, STPANode, - PastaPort, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE, STPA_NODE_TYPE, - PORT_TYPE, } from "./stpa/stpa-model"; import { StpaMouseListener } from "./stpa/stpa-mouselistener"; import { @@ -95,7 +113,7 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = bind(TYPES.HiddenVNodePostprocessor).toService(SvgPostprocessor); configureCommand({ bind, isBound }, SvgCommand); // context-menu - bind(TYPES.IContextMenuService).to(ContextMenuService); + bind(TYPES.IContextMenuService).to(ContextMenuService); bind(TYPES.IContextMenuItemProvider).to(ContextMenuProvider); // configure the diagram elements @@ -108,8 +126,8 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = // STPA configureModelElement(context, "graph", SGraph, STPAGraphView); - configureModelElement(context, INVISIBLE_NODE_TYPE, SNode, InvisibleNodeView); - configureModelElement(context, PROCESS_MODEL_NODE_TYPE, SNode, InvisibleNodeView); + configureModelElement(context, CS_INVISIBLE_SUBCOMPONENT_TYPE, SNode, InvisibleNodeView); + configureModelElement(context, PROCESS_MODEL_PARENT_NODE_TYPE, SNode, InvisibleNodeView); configureModelElement(context, DUMMY_NODE_TYPE, CSNode, CSNodeView); configureModelElement(context, CS_NODE_TYPE, CSNode, CSNodeView); configureModelElement(context, STPA_NODE_TYPE, STPANode, STPANodeView); @@ -131,7 +149,7 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = export function createPastaDiagramContainer(widgetId: string): Container { const container = new Container(); - loadDefaultModules(container, {exclude: [contextMenuModule]}); + loadDefaultModules(container, { exclude: [contextMenuModule] }); container.load(pastaContextMenuModule, pastaDiagramModule, sidebarModule, optionsModule); overrideViewerOptions(container, { needsClientLayout: true, diff --git a/extension/src-webview/stpa/stpa-model.ts b/extension/src-webview/stpa/stpa-model.ts index 7954dd19..566e6f66 100644 --- a/extension/src-webview/stpa/stpa-model.ts +++ b/extension/src-webview/stpa/stpa-model.ts @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2021-2023 by + * Copyright 2021-2024 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -21,8 +21,8 @@ import { SEdge, SNode, SPort, connectableFeature, fadeFeature, layoutContainerFe export const STPA_NODE_TYPE = 'node:stpa'; export const PARENT_TYPE = 'node:parent'; export const CS_NODE_TYPE = 'node:cs'; -export const INVISIBLE_NODE_TYPE = 'node:invisible'; -export const PROCESS_MODEL_NODE_TYPE = 'node:processModel'; +export const CS_INVISIBLE_SUBCOMPONENT_TYPE = 'node:invisibleSubcomponent'; +export const PROCESS_MODEL_PARENT_NODE_TYPE = 'node:processModelParent'; export const DUMMY_NODE_TYPE = 'node:dummy'; export const EDGE_TYPE = 'edge'; export const CS_EDGE_TYPE = 'edge:controlStructure'; @@ -63,7 +63,8 @@ export class STPAEdge extends SEdge { /** Port representing a port in the STPA graph. */ export class PastaPort extends SPort { side?: PortSide; - assocEdge?: { node1: string; node2: string }; + /** Saves start and end of the edge for which the port was created. Needed to sort the ports based on their associacted edges. */ + associatedEdge?: { node1: string; node2: string }; } /** @@ -71,7 +72,6 @@ export class PastaPort extends SPort { */ export class CSNode extends SNode { level?: number; - // processmodel? static readonly DEFAULT_FEATURES = [connectableFeature, selectFeature, layoutContainerFeature, fadeFeature]; } From 0816bf1e390ef029adc5b9e8e062d6f2369a1ca3 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 15 Feb 2024 16:40:13 +0100 Subject: [PATCH 13/16] fixed input/output translation to graph --- .../stpa/diagram/diagram-generator.ts | 11 ++++++++--- extension/src-language-server/stpa/diagram/utils.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 2204b423..26e895ae 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -412,7 +412,6 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { edges.push(...this.translateCommandsToEdges(node.actions, EdgeType.CONTROL_ACTION, args)); // create edges representing feedback edges.push(...this.translateCommandsToEdges(node.feedbacks, EdgeType.FEEDBACK, args)); - // FIXME: input/output does not work anymore // create edges representing the other inputs edges.push(...this.translateIOToEdgeAndNode(node.inputs, node, EdgeType.INPUT, args)); // create edges representing the other outputs @@ -519,7 +518,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param node The node of the inputs or outputs. * @param edgetype The type of the edge (input or output). * @param args GeneratorContext of the STPA model. - * @returns a list of edges representing the inputs or outputs. + * @returns a list of edges representing the inputs or outputs that should be added at the top level. */ protected translateIOToEdgeAndNode( io: Command[], @@ -582,7 +581,13 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { console.error("EdgeType is not INPUT or OUTPUT"); break; } - return graphComponents; + if (node.$container?.$type === "Graph") { + return graphComponents; + } else { + const parent = this.idToSNode.get(idCache.getId(node.$container)!); + const invisibleChild = parent?.children?.find(child => child.type === CS_INVISIBLE_SUBCOMPONENT_TYPE); + invisibleChild?.children?.push(...graphComponents); + } } return []; } diff --git a/extension/src-language-server/stpa/diagram/utils.ts b/extension/src-language-server/stpa/diagram/utils.ts index 3e8c3193..e5ea7b19 100644 --- a/extension/src-language-server/stpa/diagram/utils.ts +++ b/extension/src-language-server/stpa/diagram/utils.ts @@ -405,7 +405,7 @@ export function getAncestors(node: Node): (Node | Graph)[] { export function sortPorts(nodes: CSNode[]): void { for (const node of nodes) { // sort the ports of the children - const children = node.children?.filter(child => child.type.startsWith("node")) as CSNode[]; + const children = node.children?.filter(child => child.type.startsWith("node")) as CSNode[] ?? []; sortPorts(children); // separate the ports from the other children From 8c77644912f350dc4168c56df867faf21bb8c1fd Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 20 Feb 2024 09:15:03 +0100 Subject: [PATCH 14/16] mka feedback --- .../stpa/diagram/diagram-generator.ts | 18 +++++++++--------- extension/src-webview/stpa/stpa-views.tsx | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 26e895ae..19c958f4 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -112,9 +112,9 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { const sysCons = collectElementsWithSubComps(filteredModel.systemLevelConstraints); stpaChildren = stpaChildren?.concat([ ...hazards - .map(sh => + .map(hazard => this.generateAspectWithEdges( - sh, + hazard, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.HAZARDS || (showLabels === showLabelsValue.AUTOMATIC && @@ -124,9 +124,9 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) .flat(1), ...sysCons - .map(ssc => + .map(systemConstraint => this.generateAspectWithEdges( - ssc, + systemConstraint, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.SYSTEM_CONSTRAINTS || (showLabels === showLabelsValue.AUTOMATIC && @@ -140,9 +140,9 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // subcomponents are contained in the parent stpaChildren = stpaChildren?.concat([ ...filteredModel.hazards - ?.map(h => + ?.map(hazard => this.generateAspectWithEdges( - h, + hazard, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.HAZARDS || (showLabels === showLabelsValue.AUTOMATIC && @@ -152,9 +152,9 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) .flat(1), ...filteredModel.systemLevelConstraints - ?.map(sc => + ?.map(systemConstraint => this.generateAspectWithEdges( - sc, + systemConstraint, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.SYSTEM_CONSTRAINTS || (showLabels === showLabelsValue.AUTOMATIC && @@ -164,7 +164,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ) .flat(1), ...filteredModel.systemLevelConstraints - ?.map(sc => sc.subComponents?.map(ssc => this.generateEdgesForSTPANode(ssc, args))) + ?.map(systemConstraint => systemConstraint.subComponents?.map(subsystemConstraint => this.generateEdgesForSTPANode(subsystemConstraint, args))) .flat(2), ]); } diff --git a/extension/src-webview/stpa/stpa-views.tsx b/extension/src-webview/stpa/stpa-views.tsx index 563c5938..ae89436a 100644 --- a/extension/src-webview/stpa/stpa-views.tsx +++ b/extension/src-webview/stpa/stpa-views.tsx @@ -259,4 +259,4 @@ export class HeaderLabelView extends SLabelView { {super.render(label, context)} } -} \ No newline at end of file +} From 5f0671d495a1c94e046d9853c08d14bd408c5d14 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 20 Feb 2024 10:48:34 +0100 Subject: [PATCH 15/16] restructured diagram generation --- .../stpa/diagram/diagram-controlStructure.ts | 489 +++++++ .../stpa/diagram/diagram-elements.ts | 230 ++++ .../stpa/diagram/diagram-generator.ts | 1172 +---------------- .../stpa/diagram/diagram-relationshipGraph.ts | 608 +++++++++ 4 files changed, 1337 insertions(+), 1162 deletions(-) create mode 100644 extension/src-language-server/stpa/diagram/diagram-controlStructure.ts create mode 100644 extension/src-language-server/stpa/diagram/diagram-elements.ts create mode 100644 extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts diff --git a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts new file mode 100644 index 00000000..6189df36 --- /dev/null +++ b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts @@ -0,0 +1,489 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2024 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { AstNode } from "langium"; +import { GeneratorContext, IdCache } from "langium-sprotty"; +import { SModelElement, SNode } from "sprotty-protocol"; +import { Command, Graph, Model, Node, Variable, VerticalEdge } from "../../generated/ast"; +import { createControlStructureEdge, createDummyNode, createLabel, createPort } from "./diagram-elements"; +import { CSEdge, CSNode, ParentNode } from "./stpa-interfaces"; +import { + CS_EDGE_TYPE, + CS_INTERMEDIATE_EDGE_TYPE, + CS_INVISIBLE_SUBCOMPONENT_TYPE, + CS_NODE_TYPE, + EdgeType, + HEADER_LABEL_TYPE, + PARENT_TYPE, + PROCESS_MODEL_PARENT_NODE_TYPE, + PortSide, +} from "./stpa-model"; +import { StpaSynthesisOptions } from "./stpa-synthesis-options"; +import { getCommonAncestor, setLevelOfCSNodes, sortPorts } from "./utils"; + +export function createControlStructure( + controlStructure: Graph, + idToSNode: Map, + options: StpaSynthesisOptions, + args: GeneratorContext +): ParentNode { + // set the level of the nodes in the control structure automatically + setLevelOfCSNodes(controlStructure.nodes); + // determine the nodes of the control structure graph + const csNodes = controlStructure.nodes.map(n => createControlStructureNode(n, idToSNode, options, args)); + // children (nodes and edges) of the control structure + const CSChildren = [ + ...csNodes, + ...generateVerticalCSEdges(controlStructure.nodes, idToSNode, args), + //...this.generateHorizontalCSEdges(filteredModel.controlStructure.edges, args) + ]; + // sort the ports in order to group edges based on the nodes they are connected to + sortPorts(CSChildren.filter(node => node.type.startsWith("node")) as CSNode[]); + + return { + type: PARENT_TYPE, + id: "controlStructure", + children: CSChildren, + modelOrder: options.getModelOrder(), + }; +} + +/** + * Generates a single control structure node for the given {@code node}, + * @param node The system component a CSNode should be created for. + * @param param1 GeneratorContext of the STPA model. + * @returns A CSNode representing {@code node}. + */ +export function createControlStructureNode( + node: Node, + idToSNode: Map, + options: StpaSynthesisOptions, + args: GeneratorContext +): CSNode { + const idCache = args.idCache; + const label = node.label ? node.label : node.name; + const nodeId = idCache.uniqueId(node.name, node); + const children: SModelElement[] = createLabel([label], nodeId, idCache); + if (options.getShowProcessModels()) { + // add nodes representing the process model + children.push(createProcessModelNodes(node.variables, idCache)); + } + // add children of the control structure node + if (node.children?.length !== 0) { + // add invisible node to group the children in order to be able to lay them out separately from the process model node + const invisibleNode = { + type: CS_INVISIBLE_SUBCOMPONENT_TYPE, + id: idCache.uniqueId(node.name + "_invisible"), + children: [] as SModelElement[], + layout: "stack", + layoutOptions: { + paddingTop: 10.0, + paddingBottom: 10.0, + paddingLeft: 10.0, + paddingRight: 10.0, + }, + }; + // create the actual children + node.children?.forEach(child => { + invisibleNode.children?.push(createControlStructureNode(child, idToSNode, options, args)); + }); + children.push(invisibleNode); + } + const csNode = { + type: CS_NODE_TYPE, + id: nodeId, + level: node.level, + children: children, + layout: "stack", + layoutOptions: { + paddingTop: 10.0, + paddingBottom: 10.0, + paddingLeft: 10.0, + paddingRight: 10.0, + }, + }; + idToSNode.set(nodeId, csNode); + return csNode; +} + +/** + * Creates nodes representing the process model defined by the {@code variables} and encapsulates them in an invisible node. + * @param variables The variables of the process model. + * @param idCache The id cache of the STPA model. + * @returns an invisible node containing the nodes representing the process model. + */ +export function createProcessModelNodes(variables: Variable[], idCache: IdCache): SNode { + const csChildren: SModelElement[] = []; + for (const variable of variables) { + // translate the variable name to a header label and the values to further labels + const label = variable.name; + const nodeId = idCache.uniqueId(variable.name, variable); + const values = variable.values?.map(value => value.name); + const children = [ + ...createLabel([label], nodeId, idCache, HEADER_LABEL_TYPE), + ...createLabel(values, nodeId, idCache), + ]; + // create the actual node with the created labels + const csNode = { + type: CS_NODE_TYPE, + id: nodeId, + children: children, + layout: "stack", + layoutOptions: { + paddingTop: 10.0, + paddingBottom: 10.0, + paddingLeft: 10.0, + paddingRight: 10.0, + }, + } as CSNode; + csChildren.push(csNode); + } + // encapsulate the nodes representing the process model in an invisible node + const invisibleNode = { + type: PROCESS_MODEL_PARENT_NODE_TYPE, + id: idCache.uniqueId("invisible"), + children: csChildren, + layout: "stack", + layoutOptions: { + paddingTop: 10.0, + paddingBottom: 10.0, + paddingLeft: 10.0, + paddingRight: 10.0, + }, + }; + return invisibleNode; +} + +/** + * Creates the edges for the control structure. + * @param nodes The nodes of the control structure. + * @param args GeneratorContext of the STPA model + * @returns A list of edges for the control structure. + */ +export function generateVerticalCSEdges( + nodes: Node[], + idToSNode: Map, + args: GeneratorContext +): (CSNode | CSEdge)[] { + const edges: (CSNode | CSEdge)[] = []; + // for every control action and feedback of every a node, a edge should be created + for (const node of nodes) { + // create edges representing the control actions + edges.push(...translateCommandsToEdges(node.actions, EdgeType.CONTROL_ACTION, idToSNode, args)); + // create edges representing feedback + edges.push(...translateCommandsToEdges(node.feedbacks, EdgeType.FEEDBACK, idToSNode, args)); + // create edges representing the other inputs + edges.push(...translateIOToEdgeAndNode(node.inputs, node, EdgeType.INPUT, idToSNode, args)); + // create edges representing the other outputs + edges.push(...translateIOToEdgeAndNode(node.outputs, node, EdgeType.OUTPUT, idToSNode, args)); + // create edges for children and add the ones that must be added at the top level + edges.push(...generateVerticalCSEdges(node.children, idToSNode, args)); + } + return edges; +} + +/** + * Translates the commands (control action or feedback) of a node to (intermediate) edges and adds them to the correct nodes. + * @param commands The control actions or feedback of a node. + * @param edgeType The type of the edge (control action or feedback). + * @param args GeneratorContext of the STPA model. + * @returns A list of edges representing the commands that should be added at the top level. + */ +export function translateCommandsToEdges( + commands: VerticalEdge[], + edgeType: EdgeType, + idToSNode: Map, + args: GeneratorContext +): CSEdge[] { + const idCache = args.idCache; + const edges: CSEdge[] = []; + for (const edge of commands) { + // create edge id + const source = edge.$container; + const target = edge.target.ref; + const edgeId = idCache.uniqueId( + `${idCache.getId(source)}_${edge.comms[0].name}_${idCache.getId(target)}`, + edge + ); + + if (target) { + // multiple commands to same target is represented by one edge -> combine labels to one + const label: string[] = []; + for (let i = 0; i < edge.comms.length; i++) { + const com = edge.comms[i]; + label.push(com.label); + } + // edges can be hierachy crossing so we must determine the common ancestor of source and target + const commonAncestor = getCommonAncestor(source, target); + // create the intermediate ports and edges + const ports = generateIntermediateCSEdges( + source, + target, + edgeId, + edgeType, + idToSNode, + args, + commonAncestor + ); + // add edge between the two ports in the common ancestor + const csEdge = createControlStructureEdge( + idCache.uniqueId(edgeId), + ports.sourcePort, + ports.targetPort, + label, + edgeType, + // if the common ancestor is the parent of the target we want an edge with an arrow otherwise an intermediate edge + target.$container === commonAncestor ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE, + args + ); + if (commonAncestor?.$type === "Graph") { + // if the common ancestor is the graph, the edge must be added at the top level and hence have to be returned + edges.push(csEdge); + } else if (commonAncestor) { + // if the common ancestor is a node, the edge must be added to the children of the common ancestor + const snodeAncestor = idToSNode.get(idCache.getId(commonAncestor)!); + snodeAncestor?.children + ?.find(node => node.type === CS_INVISIBLE_SUBCOMPONENT_TYPE) + ?.children?.push(csEdge); + } + } + } + return edges; +} + +/** + * Translates the inputs or outputs of a node to edges. + * @param io The inputs or outputs of a node. + * @param node The node of the inputs or outputs. + * @param edgetype The type of the edge (input or output). + * @param args GeneratorContext of the STPA model. + * @returns a list of edges representing the inputs or outputs that should be added at the top level. + */ +export function translateIOToEdgeAndNode( + io: Command[], + node: Node, + edgetype: EdgeType, + idToSNode: Map, + args: GeneratorContext +): (CSNode | CSEdge)[] { + if (io.length !== 0) { + const idCache = args.idCache; + const nodeId = idCache.getId(node); + + // create the label of the edge + const label: string[] = []; + for (let i = 0; i < io.length; i++) { + const command = io[i]; + label.push(command.label); + } + + let graphComponents: (CSNode | CSEdge)[] = []; + switch (edgetype) { + case EdgeType.INPUT: + // create dummy node for the input + const inputDummyNode = createDummyNode( + "input" + node.name, + node.level ? node.level - 1 : undefined, + idCache + ); + // create edge for the input + const inputEdge = createControlStructureEdge( + idCache.uniqueId(`${inputDummyNode.id}_input_${nodeId}`), + inputDummyNode.id ? inputDummyNode.id : "", + nodeId ? nodeId : "", + label, + edgetype, + CS_EDGE_TYPE, + args + ); + graphComponents = [inputEdge, inputDummyNode]; + break; + case EdgeType.OUTPUT: + // create dummy node for the output + const outputDummyNode = createDummyNode( + "output" + node.name, + node.level ? node.level + 1 : undefined, + idCache + ); + // create edge for the output + const outputEdge = createControlStructureEdge( + idCache.uniqueId(`${nodeId}_output_${outputDummyNode.id}`), + nodeId ? nodeId : "", + outputDummyNode.id ? outputDummyNode.id : "", + label, + edgetype, + CS_EDGE_TYPE, + args + ); + graphComponents = [outputEdge, outputDummyNode]; + break; + default: + console.error("EdgeType is not INPUT or OUTPUT"); + break; + } + if (node.$container?.$type === "Graph") { + return graphComponents; + } else { + const parent = idToSNode.get(idCache.getId(node.$container)!); + const invisibleChild = parent?.children?.find(child => child.type === CS_INVISIBLE_SUBCOMPONENT_TYPE); + invisibleChild?.children?.push(...graphComponents); + } + } + return []; +} + +/** + * Generates intermediate edges and ports for the given {@code source} and {@code target} to connect them through hierarchical levels. + * @param source The source of the edge. + * @param target The target of the edge. + * @param edgeId The ID of the original edge. + * @param edgeType The type of the edge. + * @param args The GeneratorContext of the STPA model. + * @param ancestor The common ancestor of the source and target. + * @returns the IDs of the source and target port at the hierarchy level of the {@code ancestor}. + */ +export function generateIntermediateCSEdges( + source: Node | undefined, + target: Node | undefined, + edgeId: string, + edgeType: EdgeType, + idToSNode: Map, + args: GeneratorContext, + ancestor?: Node | Graph +): { sourcePort: string; targetPort: string } { + const assocEdge = { node1: source?.name ?? "", node2: target?.name ?? "" }; + // add ports for source and target and their ancestors till the common ancestor + const sources = generatePortsForCSHierarchy( + source, + assocEdge, + edgeId, + edgeType === EdgeType.CONTROL_ACTION ? PortSide.SOUTH : PortSide.NORTH, + idToSNode, + args.idCache, + ancestor + ); + const targets = generatePortsForCSHierarchy( + target, + assocEdge, + edgeId, + edgeType === EdgeType.CONTROL_ACTION ? PortSide.NORTH : PortSide.SOUTH, + idToSNode, + args.idCache, + ancestor + ); + // add edges between the ports of the source and its ancestors + for (let i = 0; i < sources.nodes.length - 1; i++) { + const sEdgeType = CS_INTERMEDIATE_EDGE_TYPE; + sources.nodes[i + 1]?.children?.push( + createControlStructureEdge( + args.idCache.uniqueId(edgeId), + sources.portIds[i], + sources.portIds[i + 1], + [], + edgeType, + sEdgeType, + args, + false + ) + ); + } + // add edges between the ports of the target and its ancestors + for (let i = 0; i < targets.nodes.length - 1; i++) { + const sEdgeType = i === 0 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE; + targets.nodes[i + 1]?.children?.push( + createControlStructureEdge( + args.idCache.uniqueId(edgeId), + targets.portIds[i + 1], + targets.portIds[i], + [], + edgeType, + sEdgeType, + args, + false + ) + ); + } + // return the source and target port at the hierarchy level of the ancestor + return { + sourcePort: sources.portIds[sources.portIds.length - 1], + targetPort: targets.portIds[targets.portIds.length - 1], + }; +} + +/** + * Adds ports for the {@code current} node and its (grand)parents up to the {@code ancestor}. + * @param current The node for which the ports should be created. + * @param assocEdge The associated edge for which the ports should be created. + * @param edgeId The ID of the original edge for which the ports should be created. + * @param side The side of the ports. + * @param idCache The ID cache of the STPA model. + * @param ancestor The common ancestor of the source and target of the associated edge. + * @returns the IDs of the created ports and the nodes the ports were added to. + */ +export function generatePortsForCSHierarchy( + current: AstNode | undefined, + assocEdge: { node1: string; node2: string }, + edgeId: string, + side: PortSide, + idToSNode: Map, + idCache: IdCache, + ancestor?: Node | Graph +): { portIds: string[]; nodes: SNode[] } { + const ids: string[] = []; + const nodes: SNode[] = []; + while (current && (!ancestor || current !== ancestor)) { + const currentId = idCache.getId(current); + if (currentId) { + const currentNode = idToSNode.get(currentId); + if (currentNode) { + // current node could have an invisible child that was skipped while going up the hierarchy because it does not exist in the AST + const invisibleChild = currentNode?.children?.find( + child => child.type === CS_INVISIBLE_SUBCOMPONENT_TYPE + ); + if (invisibleChild && ids.length !== 0) { + // add port for the invisible node first + const invisiblePortId = idCache.uniqueId(edgeId + "_newTransition"); + invisibleChild.children?.push(createPort(invisiblePortId, side, assocEdge)); + ids.push(invisiblePortId); + nodes.push(invisibleChild); + } + // add port for the current node + const nodePortId = idCache.uniqueId(edgeId + "_newTransition"); + currentNode?.children?.push(createPort(nodePortId, side, assocEdge)); + ids.push(nodePortId); + nodes.push(currentNode); + current = current?.$container; + } + } + } + return { portIds: ids, nodes: nodes }; +} + +// for this in-layer edges are needed, which are not supported by ELK at the moment +/* protected generateHorizontalCSEdges(edges: Edge[], args: GeneratorContext): SEdge[]{ + const idCache = args.idCache + let genEdges: SEdge[] = [] + for (const edge of edges) { + const sourceId = idCache.getId(edge.source.ref) + const targetId = idCache.getId(edge.target.ref) + const edgeId = idCache.uniqueId(`${sourceId}:${edge.name}:${targetId}`, edge) + const e = this.generateSEdge(edgeId, sourceId ? sourceId : '', targetId ? targetId : '', + edge.label? edge.label:edge.name, args) + genEdges.push(e) + } + return genEdges + } */ diff --git a/extension/src-language-server/stpa/diagram/diagram-elements.ts b/extension/src-language-server/stpa/diagram/diagram-elements.ts new file mode 100644 index 00000000..2fcca8c9 --- /dev/null +++ b/extension/src-language-server/stpa/diagram/diagram-elements.ts @@ -0,0 +1,230 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2024 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { AstNode } from "langium"; +import { GeneratorContext, IdCache } from "langium-sprotty"; +import { SLabel, SModelElement } from "sprotty-protocol"; +import { Model } from "../../generated/ast"; +import { getDescription } from "../../utils"; +import { CSEdge, CSNode, PastaPort, STPAEdge, STPANode } from "./stpa-interfaces"; +import { DUMMY_NODE_TYPE, EdgeType, PORT_TYPE, PortSide, STPAAspect, STPA_NODE_TYPE } from "./stpa-model"; +import { getAspect } from "./utils"; +import { StpaSynthesisOptions } from "./stpa-synthesis-options"; + +/** + * Creates an STPANode. + * @param node The AstNode for which the STPANode should be created. + * @param nodeId The ID of the STPANode. + * @param lvl The hierarchy level of the STPANode. + * @param children The children of the STPANode. + * @returns an STPANode. + */ +export function createSTPANode( + node: AstNode, + nodeId: string, + lvl: number, + description: string, + children: SModelElement[], + options: StpaSynthesisOptions +): STPANode { + return { + type: STPA_NODE_TYPE, + id: nodeId, + aspect: getAspect(node), + description: description, + hierarchyLvl: lvl, + children: children, + layout: "stack", + layoutOptions: { + paddingTop: 10.0, + paddingBottom: 10.0, + paddingLeft: 10.0, + paddingRight: 10.0, + }, + modelOrder: options.getModelOrder(), + }; +} + +/** + * Creates a port. + * @param id The ID of the port. + * @param side The side of the port. + * @returns a port. + */ +export function createPort(id: string, side: PortSide, assocEdge?: { node1: string; node2: string }): PastaPort { + return { + type: PORT_TYPE, + id: id, + side: side, + associatedEdge: assocEdge, + }; +} + +/** + * Creates an STPAEdge. + * @param id The ID of the edge. + * @param sourceId The ID of the source of the edge. + * @param targetId The ID of the target of the edge. + * @param children The children of the edge. + * @param type The type of the edge. + * @param aspect The aspect of the edge. + * @returns an STPAEdge. + */ +export function createSTPAEdge( + id: string, + sourceId: string, + targetId: string, + children: SModelElement[], + type: string, + aspect: STPAAspect +): STPAEdge { + return { + type: type, + id: id, + sourceId: sourceId, + targetId: targetId, + children: children, + aspect: aspect, + }; +} + +/** + * Creates a control structure edge based on the given arguments. + * @param edgeId The ID of the edge that should be created. + * @param sourceId The ID of the source of the edge. + * @param targetId The ID of the target of the edge. + * @param label The labels of the edge. + * @param edgeType The type of the edge (control action or feedback edge). + * @param param5 GeneratorContext of the STPA model. + * @returns A control structure edge. + */ +export function createControlStructureEdge( + edgeId: string, + sourceId: string, + targetId: string, + label: string[], + edgeType: EdgeType, + sedgeType: string, + args: GeneratorContext, + dummyLabel: boolean = true +): CSEdge { + return { + type: sedgeType, + id: edgeId, + sourceId: sourceId!, + targetId: targetId!, + edgeType: edgeType, + children: createLabel(label, edgeId, args.idCache, undefined, dummyLabel), + }; +} + +/** + * Generates SLabel elements for the given {@code label}. + * @param label Labels to translate to SLabel elements. + * @param id The ID of the element for which the label should be generated. + * @param idCache The ID cache of the STPA model. + * @param type The type of the label. + * @param dummyLabel Determines whether a dummy label should be created to get a correct layout. + * @returns SLabel elements representing {@code label}. + */ +export function createLabel( + label: string[], + id: string, + idCache: IdCache, + type: string = "label:xref", + dummyLabel: boolean = true +): SLabel[] { + const children: SLabel[] = []; + if (label.find(l => l !== "")) { + label.forEach(l => { + children.push({ + type: type, + id: idCache.uniqueId(id + "_label"), + text: l, + } as SLabel); + }); + } else if (dummyLabel) { + // needed for correct layout + children.push({ + type: type, + id: idCache.uniqueId(id + "_label"), + text: " ", + } as SLabel); + } + return children; +} + +/** + * Creates a dummy node. + * @param idCache The ID cache of the STPA model. + * @param level The level of the dummy node. + * @returns a dummy node. + */ +export function createDummyNode(name: string, level: number | undefined, idCache: IdCache): CSNode { + const dummyNode: CSNode = { + type: DUMMY_NODE_TYPE, + id: idCache.uniqueId("dummy" + name), + layout: "stack", + layoutOptions: { + paddingTop: 10.0, + paddingBottom: 10.0, + paddngLeft: 10.0, + paddingRight: 10.0, + }, + }; + if (level) { + dummyNode.level = level; + } + return dummyNode; +} + +/** + * Generates the labels for the given node based on {@code showDescription} and the label synthesis options. + * @param showDescription Determines whether the description should be shown. + * @param nodeId The ID of the node for which the labels should be generated. + * @param nodeName The name of the node for which the labels should be generated. + * @param idCache The ID cache of the STPA model. + * @param nodeDescription The description of the node for which the labels should be generated. + * @returns the labels for the given node. + */ +export function generateDescriptionLabels( + showDescription: boolean, + nodeId: string, + nodeName: string, + options: StpaSynthesisOptions, + idCache: IdCache, + nodeDescription?: string +): SModelElement[] { + let children: SModelElement[] = []; + if (nodeDescription && showDescription) { + children = getDescription( + nodeDescription ?? "", + options.getLabelManagement(), + options.getLabelShorteningWidth(), + nodeId, + idCache + ); + } + + // show the name in the top line + children.push({ + type: "label", + id: idCache.uniqueId(nodeId + "_label"), + text: nodeName, + }); + return children; +} diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 19c958f4..4e6e9a1b 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -15,57 +15,14 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { AstNode } from "langium"; -import { GeneratorContext, IdCache, LangiumDiagramGenerator } from "langium-sprotty"; -import { SLabel, SModelElement, SModelRoot, SNode } from "sprotty-protocol"; - -import { - Command, - Graph, - Hazard, - Model, - Node, - SystemConstraint, - Variable, - VerticalEdge, - isContext, - isHazard, - isSystemConstraint, - isUCA, -} from "../../generated/ast"; -import { getDescription } from "../../utils"; +import { GeneratorContext, LangiumDiagramGenerator } from "langium-sprotty"; +import { SModelElement, SModelRoot, SNode } from "sprotty-protocol"; +import { Model } from "../../generated/ast"; import { StpaServices } from "../stpa-module"; -import { collectElementsWithSubComps, leafElement } from "../utils"; +import { createControlStructure } from "./diagram-controlStructure"; +import { createRelationshipGraph } from "./diagram-relationshipGraph"; import { filterModel } from "./filtering"; -import { CSEdge, CSNode, ParentNode, PastaPort, STPAEdge, STPANode } from "./stpa-interfaces"; -import { - CS_EDGE_TYPE, - CS_INTERMEDIATE_EDGE_TYPE, - CS_INVISIBLE_SUBCOMPONENT_TYPE, - CS_NODE_TYPE, - DUMMY_NODE_TYPE, - EdgeType, - HEADER_LABEL_TYPE, - PARENT_TYPE, - PORT_TYPE, - PROCESS_MODEL_PARENT_NODE_TYPE, - PortSide, - STPAAspect, - STPA_EDGE_TYPE, - STPA_INTERMEDIATE_EDGE_TYPE, - STPA_NODE_TYPE, -} from "./stpa-model"; -import { StpaSynthesisOptions, showLabelsValue } from "./stpa-synthesis-options"; -import { - createUCAContextDescription, - getAspect, - getAspectsThatShouldHaveDesriptions, - getCommonAncestor, - getTargets, - setLevelOfCSNodes, - setLevelsForSTPANodes, - sortPorts, -} from "./utils"; +import { StpaSynthesisOptions } from "./stpa-synthesis-options"; export class StpaDiagramGenerator extends LangiumDiagramGenerator { protected readonly options: StpaSynthesisOptions; @@ -89,206 +46,15 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // filter model based on the options set by the user const filteredModel = filterModel(model, this.options); - const showLabels = this.options.getShowLabels(); - // aspects that should have a description when showLabel option is set to automatic - const aspectsToShowDescriptions = getAspectsThatShouldHaveDesriptions(model); - - // determine the children for the STPA graph - // for each component a node is generated with edges representing the references of the component - // in order to be able to set the target IDs of the edges, the nodes must be created in the correct order - let stpaChildren: SModelElement[] = filteredModel.losses?.map(l => - this.generateSTPANode( - l, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.LOSSES || - (showLabels === showLabelsValue.AUTOMATIC && aspectsToShowDescriptions.includes(STPAAspect.LOSS)), - args - ) - ); - // the hierarchy option determines whether subcomponents are contained in ther parent or not - if (!this.options.getHierarchy()) { - // subcomponents have edges to the parent - const hazards = collectElementsWithSubComps(filteredModel.hazards); - const sysCons = collectElementsWithSubComps(filteredModel.systemLevelConstraints); - stpaChildren = stpaChildren?.concat([ - ...hazards - .map(hazard => - this.generateAspectWithEdges( - hazard, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.HAZARDS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.HAZARD)), - args - ) - ) - .flat(1), - ...sysCons - .map(systemConstraint => - this.generateAspectWithEdges( - systemConstraint, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.SYSTEM_CONSTRAINTS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.SYSTEMCONSTRAINT)), - args - ) - ) - .flat(1), - ]); - } else { - // subcomponents are contained in the parent - stpaChildren = stpaChildren?.concat([ - ...filteredModel.hazards - ?.map(hazard => - this.generateAspectWithEdges( - hazard, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.HAZARDS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.HAZARD)), - args - ) - ) - .flat(1), - ...filteredModel.systemLevelConstraints - ?.map(systemConstraint => - this.generateAspectWithEdges( - systemConstraint, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.SYSTEM_CONSTRAINTS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.SYSTEMCONSTRAINT)), - args - ) - ) - .flat(1), - ...filteredModel.systemLevelConstraints - ?.map(systemConstraint => systemConstraint.subComponents?.map(subsystemConstraint => this.generateEdgesForSTPANode(subsystemConstraint, args))) - .flat(2), - ]); - } - stpaChildren = stpaChildren?.concat([ - ...filteredModel.responsibilities - ?.map(r => - r.responsiblitiesForOneSystem.map(resp => - this.generateAspectWithEdges( - resp, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.RESPONSIBILITIES || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.RESPONSIBILITY)), - args - ) - ) - ) - .flat(2), - ...filteredModel.allUCAs - ?.map(sysUCA => - sysUCA.providingUcas - .concat(sysUCA.notProvidingUcas, sysUCA.wrongTimingUcas, sysUCA.continousUcas) - .map(uca => - this.generateAspectWithEdges( - uca, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.UCAS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.UCA)), - args - ) - ) - ) - .flat(2), - ...filteredModel.rules - ?.map(rule => - rule.contexts.map(context => - this.generateAspectWithEdges( - context, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.UCAS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.UCA)), - args - ) - ) - ) - .flat(2), - ...filteredModel.controllerConstraints - ?.map(c => - this.generateAspectWithEdges( - c, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.CONTROLLER_CONSTRAINTS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.CONTROLLERCONSTRAINT)), - args - ) - ) - .flat(1), - ...filteredModel.scenarios - ?.map(s => - this.generateAspectWithEdges( - s, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.SCENARIOS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.SCENARIO)), - args - ) - ) - .flat(1), - ...filteredModel.safetyCons - ?.map(sr => - this.generateAspectWithEdges( - sr, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.SAFETY_CONSTRAINTS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.SAFETYREQUIREMENT)), - args - ) - ) - .flat(1), - ]); - - // filtering the nodes of the STPA graph - const stpaNodes: STPANode[] = []; - for (const node of stpaChildren ?? []) { - if (node.type === STPA_NODE_TYPE) { - stpaNodes.push(node as STPANode); - } - } - // each node should be placed in a specific layer based on the aspect. therefore positions must be set - setLevelsForSTPANodes(stpaNodes, this.options.getGroupingUCAs()); - const rootChildren: SModelElement[] = []; if (filteredModel.controlStructure) { - setLevelOfCSNodes(filteredModel.controlStructure?.nodes); - // determine the nodes of the control structure graph - const csNodes = filteredModel.controlStructure?.nodes.map(n => this.createControlStructureNode(n, args)); - // children (nodes and edges) of the control structure - const CSChildren = [ - ...csNodes, - ...this.generateVerticalCSEdges(filteredModel.controlStructure.nodes, args), - //...this.generateHorizontalCSEdges(filteredModel.controlStructure.edges, args) - ]; - // sort the ports in order to group edges based on the nodes they are connected to - sortPorts(CSChildren.filter(node => node.type.startsWith("node")) as CSNode[]); // add control structure to roots children - rootChildren.push({ - type: PARENT_TYPE, - id: "controlStructure", - children: CSChildren, - modelOrder: this.options.getModelOrder(), - } as ParentNode); + rootChildren.push( + createControlStructure(filteredModel.controlStructure, this.idToSNode, this.options, args) + ); } // add relationship graph to roots children - rootChildren.push({ - type: PARENT_TYPE, - id: "relationships", - children: stpaChildren, - modelOrder: this.options.getModelOrder(), - } as ParentNode); + rootChildren.push(createRelationshipGraph(filteredModel, model, this.idToSNode, this.options, args)); // return root return { type: "graph", @@ -296,922 +62,4 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { children: rootChildren, }; } - - /** - * Generates a single control structure node for the given {@code node}, - * @param node The system component a CSNode should be created for. - * @param param1 GeneratorContext of the STPA model. - * @returns A CSNode representing {@code node}. - */ - protected createControlStructureNode(node: Node, args: GeneratorContext): CSNode { - const idCache = args.idCache; - const label = node.label ? node.label : node.name; - const nodeId = idCache.uniqueId(node.name, node); - const children: SModelElement[] = this.createLabel([label], nodeId, idCache); - if (this.options.getShowProcessModels()) { - // add nodes representing the process model - children.push(this.createProcessModelNodes(node.variables, idCache)); - } - // add children of the control structure node - if (node.children?.length !== 0) { - // add invisible node to group the children in order to be able to lay them out separately from the process model node - const invisibleNode = { - type: CS_INVISIBLE_SUBCOMPONENT_TYPE, - id: idCache.uniqueId(node.name + "_invisible"), - children: [] as SModelElement[], - layout: "stack", - layoutOptions: { - paddingTop: 10.0, - paddingBottom: 10.0, - paddingLeft: 10.0, - paddingRight: 10.0, - }, - }; - // create the actual children - node.children?.forEach(child => { - invisibleNode.children?.push(this.createControlStructureNode(child, args)); - }); - children.push(invisibleNode); - } - const csNode = { - type: CS_NODE_TYPE, - id: nodeId, - level: node.level, - children: children, - layout: "stack", - layoutOptions: { - paddingTop: 10.0, - paddingBottom: 10.0, - paddingLeft: 10.0, - paddingRight: 10.0, - }, - }; - this.idToSNode.set(nodeId, csNode); - return csNode; - } - - /** - * Creates nodes representing the process model defined by the {@code variables} and encapsulates them in an invisible node. - * @param variables The variables of the process model. - * @param idCache The id cache of the STPA model. - * @returns an invisible node containing the nodes representing the process model. - */ - protected createProcessModelNodes(variables: Variable[], idCache: IdCache): SNode { - const csChildren: SModelElement[] = []; - for (const variable of variables) { - // translate the variable name to a header label and the values to further labels - const label = variable.name; - const nodeId = idCache.uniqueId(variable.name, variable); - const values = variable.values?.map(value => value.name); - const children = [ - ...this.createLabel([label], nodeId, idCache, HEADER_LABEL_TYPE), - ...this.createLabel(values, nodeId, idCache), - ]; - // create the actual node with the created labels - const csNode = { - type: CS_NODE_TYPE, - id: nodeId, - children: children, - layout: "stack", - layoutOptions: { - paddingTop: 10.0, - paddingBottom: 10.0, - paddingLeft: 10.0, - paddingRight: 10.0, - }, - } as CSNode; - csChildren.push(csNode); - } - // encapsulate the nodes representing the process model in an invisible node - const invisibleNode = { - type: PROCESS_MODEL_PARENT_NODE_TYPE, - id: idCache.uniqueId("invisible"), - children: csChildren, - layout: "stack", - layoutOptions: { - paddingTop: 10.0, - paddingBottom: 10.0, - paddingLeft: 10.0, - paddingRight: 10.0, - }, - }; - return invisibleNode; - } - - /** - * Creates the edges for the control structure. - * @param nodes The nodes of the control structure. - * @param args GeneratorContext of the STPA model - * @returns A list of edges for the control structure. - */ - protected generateVerticalCSEdges(nodes: Node[], args: GeneratorContext): (CSNode | CSEdge)[] { - const edges: (CSNode | CSEdge)[] = []; - // for every control action and feedback of every a node, a edge should be created - for (const node of nodes) { - // create edges representing the control actions - edges.push(...this.translateCommandsToEdges(node.actions, EdgeType.CONTROL_ACTION, args)); - // create edges representing feedback - edges.push(...this.translateCommandsToEdges(node.feedbacks, EdgeType.FEEDBACK, args)); - // create edges representing the other inputs - edges.push(...this.translateIOToEdgeAndNode(node.inputs, node, EdgeType.INPUT, args)); - // create edges representing the other outputs - edges.push(...this.translateIOToEdgeAndNode(node.outputs, node, EdgeType.OUTPUT, args)); - // create edges for children and add the ones that must be added at the top level - edges.push(...this.generateVerticalCSEdges(node.children, args)); - } - return edges; - } - - /** - * Translates the commands (control action or feedback) of a node to (intermediate) edges and adds them to the correct nodes. - * @param commands The control actions or feedback of a node. - * @param edgeType The type of the edge (control action or feedback). - * @param args GeneratorContext of the STPA model. - * @returns A list of edges representing the commands that should be added at the top level. - */ - protected translateCommandsToEdges( - commands: VerticalEdge[], - edgeType: EdgeType, - args: GeneratorContext - ): CSEdge[] { - const idCache = args.idCache; - const edges: CSEdge[] = []; - for (const edge of commands) { - // create edge id - const source = edge.$container; - const target = edge.target.ref; - const edgeId = idCache.uniqueId( - `${idCache.getId(source)}_${edge.comms[0].name}_${idCache.getId(target)}`, - edge - ); - - if (target) { - // multiple commands to same target is represented by one edge -> combine labels to one - const label: string[] = []; - for (let i = 0; i < edge.comms.length; i++) { - const com = edge.comms[i]; - label.push(com.label); - } - // edges can be hierachy crossing so we must determine the common ancestor of source and target - const commonAncestor = getCommonAncestor(source, target); - // create the intermediate ports and edges - const ports = this.generateIntermediateCSEdges(source, target, edgeId, edgeType, args, commonAncestor); - // add edge between the two ports in the common ancestor - const csEdge = this.createControlStructureEdge( - idCache.uniqueId(edgeId), - ports.sourcePort, - ports.targetPort, - label, - edgeType, - // if the common ancestor is the parent of the target we want an edge with an arrow otherwise an intermediate edge - target.$container === commonAncestor ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE, - args - ); - if (commonAncestor?.$type === "Graph") { - // if the common ancestor is the graph, the edge must be added at the top level and hence have to be returned - edges.push(csEdge); - } else if (commonAncestor) { - // if the common ancestor is a node, the edge must be added to the children of the common ancestor - const snodeAncestor = this.idToSNode.get(idCache.getId(commonAncestor)!); - snodeAncestor?.children - ?.find(node => node.type === CS_INVISIBLE_SUBCOMPONENT_TYPE) - ?.children?.push(csEdge); - } - } - } - return edges; - } - - /** - * Create the source and target port for the edge with the given {@code edgeId}. - * @param sourceId The id of the source node. - * @param sourceSide The side of the source node the edge should be connected to. - * @param targetId The id of the target node. - * @param targetSide The side of the target node the edge should be connected to. - * @param edgeId The id of the edge. - * @param idCache The id cache of the STPA model. - * @returns the ids of the source and target port the edge should be connected to. - */ - protected createPortsForEdge( - sourceId: string, - sourceSide: PortSide, - targetId: string, - targetSide: PortSide, - edgeId: string, - idCache: IdCache - ): { sourcePortId: string; targetPortId: string } { - // add ports for source and target - const sourceNode = this.idToSNode.get(sourceId); - const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); - sourceNode?.children?.push(this.createPort(sourcePortId, sourceSide)); - - const targetNode = this.idToSNode.get(targetId!); - const targetPortId = idCache.uniqueId(edgeId + "_newTransition"); - targetNode?.children?.push(this.createPort(targetPortId, targetSide)); - - return { sourcePortId, targetPortId }; - } - - /** - * Translates the inputs or outputs of a node to edges. - * @param io The inputs or outputs of a node. - * @param node The node of the inputs or outputs. - * @param edgetype The type of the edge (input or output). - * @param args GeneratorContext of the STPA model. - * @returns a list of edges representing the inputs or outputs that should be added at the top level. - */ - protected translateIOToEdgeAndNode( - io: Command[], - node: Node, - edgetype: EdgeType, - args: GeneratorContext - ): (CSNode | CSEdge)[] { - if (io.length !== 0) { - const idCache = args.idCache; - const nodeId = idCache.getId(node); - - // create the label of the edge - const label: string[] = []; - for (let i = 0; i < io.length; i++) { - const command = io[i]; - label.push(command.label); - } - - let graphComponents: (CSNode | CSEdge)[] = []; - switch (edgetype) { - case EdgeType.INPUT: - // create dummy node for the input - const inputDummyNode = this.createDummyNode( - "input" + node.name, - node.level ? node.level - 1 : undefined, - idCache - ); - // create edge for the input - const inputEdge = this.createControlStructureEdge( - idCache.uniqueId(`${inputDummyNode.id}_input_${nodeId}`), - inputDummyNode.id ? inputDummyNode.id : "", - nodeId ? nodeId : "", - label, - edgetype, - CS_EDGE_TYPE, - args - ); - graphComponents = [inputEdge, inputDummyNode]; - break; - case EdgeType.OUTPUT: - // create dummy node for the output - const outputDummyNode = this.createDummyNode( - "output" + node.name, - node.level ? node.level + 1 : undefined, - idCache - ); - // create edge for the output - const outputEdge = this.createControlStructureEdge( - idCache.uniqueId(`${nodeId}_output_${outputDummyNode.id}`), - nodeId ? nodeId : "", - outputDummyNode.id ? outputDummyNode.id : "", - label, - edgetype, - CS_EDGE_TYPE, - args - ); - graphComponents = [outputEdge, outputDummyNode]; - break; - default: - console.error("EdgeType is not INPUT or OUTPUT"); - break; - } - if (node.$container?.$type === "Graph") { - return graphComponents; - } else { - const parent = this.idToSNode.get(idCache.getId(node.$container)!); - const invisibleChild = parent?.children?.find(child => child.type === CS_INVISIBLE_SUBCOMPONENT_TYPE); - invisibleChild?.children?.push(...graphComponents); - } - } - return []; - } - - // for this in-layer edges are needed, which are not supported by ELK at the moment - /* protected generateHorizontalCSEdges(edges: Edge[], args: GeneratorContext): SEdge[]{ - const idCache = args.idCache - let genEdges: SEdge[] = [] - for (const edge of edges) { - const sourceId = idCache.getId(edge.source.ref) - const targetId = idCache.getId(edge.target.ref) - const edgeId = idCache.uniqueId(`${sourceId}:${edge.name}:${targetId}`, edge) - const e = this.generateSEdge(edgeId, sourceId ? sourceId : '', targetId ? targetId : '', - edge.label? edge.label:edge.name, args) - genEdges.push(e) - } - return genEdges - } */ - - /** - * Generates a node and the edges for the given {@code node}. - * @param node STPA component for which a node and edges should be generated. - * @param args GeneratorContext of the STPA model. - * @returns A node representing {@code node} and edges representing the references {@code node} contains. - */ - protected generateAspectWithEdges( - node: leafElement, - showDescription: boolean, - args: GeneratorContext - ): SModelElement[] { - // node must be created first in order to access the id when creating the edges - const stpaNode = this.generateSTPANode(node, showDescription, args); - // uca nodes need to save their control action in order to be able to group them by the actions - if ((isUCA(node) || isContext(node)) && node.$container.system.ref) { - stpaNode.controlAction = node.$container.system.ref.name + "." + node.$container.action.ref?.name; - } - const elements: SModelElement[] = this.generateEdgesForSTPANode(node, args); - elements.push(stpaNode); - return elements; - } - - /** - * Generates a single STPANode for the given {@code node}. - * @param node The STPA component the node should be created for. - * @param args GeneratorContext of the STPA model. - * @returns A STPANode representing {@code node}. - */ - protected generateSTPANode(node: leafElement, showDescription: boolean, args: GeneratorContext): STPANode { - const idCache = args.idCache; - const nodeId = idCache.uniqueId(node.name, node); - // determines the hierarchy level for subcomponents. For other components the value is 0. - let lvl = 0; - let container = node.$container; - while (isHazard(container) || isSystemConstraint(container)) { - lvl++; - container = container.$container; - } - - let children: SModelElement[] = this.generateDescriptionLabels( - showDescription, - nodeId, - node.name, - args.idCache, - isContext(node) ? createUCAContextDescription(node) : node.description - ); - // if the hierarchy option is true, the subcomponents are added as children to the parent - if (this.options.getHierarchy() && isHazard(node) && node.subComponents.length !== 0) { - // adds subhazards - children = children.concat( - node.subComponents?.map((sc: Hazard) => this.generateSTPANode(sc, showDescription, args)) - ); - } - if (this.options.getHierarchy() && isSystemConstraint(node) && node.subComponents.length !== 0) { - // adds subconstraints - children = children.concat( - node.subComponents?.map((sc: SystemConstraint) => this.generateSTPANode(sc, showDescription, args)) - ); - } - - if (isContext(node)) { - // context UCAs have no description - const result = this.createSTPANode(node, nodeId, lvl, "", children); - this.idToSNode.set(nodeId, result); - return result; - } else { - const result = this.createSTPANode(node, nodeId, lvl, node.description, children); - this.idToSNode.set(nodeId, result); - return result; - } - } - - /** - * Generates the edges for {@code node}. - * @param node STPA component for which the edges should be created. - * @param args GeneratorContext of the STPA model. - * @returns Edges representing the references {@code node} contains. - */ - protected generateEdgesForSTPANode(node: AstNode, args: GeneratorContext): SModelElement[] { - const elements: SModelElement[] = []; - // for every reference an edge is created - // if hierarchy option is false, edges from subcomponents to parents are created too - const targets = getTargets(node, this.options.getHierarchy()); - for (const target of targets) { - const edge = this.generateSTPAEdge(node, target, "", args); - if (edge) { - elements.push(edge); - } - } - return elements; - } - - /** - * Generates a single STPAEdge based on the given arguments. - * @param source The source of the edge. - * @param target The target of the edge. - * @param label The label of the edge. - * @param param4 GeneratorContext of the STPA model. - * @returns An STPAEdge. - */ - protected generateSTPAEdge( - source: AstNode, - target: AstNode, - label: string, - { idCache }: GeneratorContext - ): STPAEdge | undefined { - // get the IDs - const targetId = idCache.getId(target); - const sourceId = idCache.getId(source); - const edgeId = idCache.uniqueId(`${sourceId}_${targetId}`, undefined); - - if (sourceId && targetId) { - // create the label of the edge - let children: SModelElement[] = []; - if (label !== "") { - children = this.createLabel([label], edgeId, idCache); - } - - if ((isHazard(target) || isSystemConstraint(target)) && target.$container?.$type !== "Model") { - // if the target is a subcomponent we need to add several ports and edges through the hierarchical structure - return this.generateIntermediateIncomingSTPAEdges(target, source, sourceId, edgeId, children, idCache); - } else { - // otherwise it is sufficient to add ports for source and target - const portIds = this.createPortsForEdge( - sourceId, - PortSide.NORTH, - targetId, - PortSide.SOUTH, - edgeId, - idCache - ); - - // add edge between the two ports - return this.createSTPAEdge( - edgeId, - portIds.sourcePortId, - portIds.targetPortId, - children, - STPA_EDGE_TYPE, - getAspect(source) - ); - } - } - } - - /** - * Generates incoming edges between the {@code source}, the top parent(s), and the {@code target}. - * @param target The target of the edge. - * @param source The source of the edge. - * @param sourceId The ID of the source of the edge. - * @param edgeId The ID of the original edge. - * @param children The children of the original edge. - * @param idCache The ID cache of the STPA model. - * @returns an STPAEdge to connect the {@code source} (or its top parent) with the top parent of the {@code target}. - */ - protected generateIntermediateIncomingSTPAEdges( - target: AstNode, - source: AstNode, - sourceId: string, - edgeId: string, - children: SModelElement[], - idCache: IdCache - ): STPAEdge { - // add ports to the target and its (grand)parents - const targetPortIds = this.generatePortsForHierarchy(target, edgeId, PortSide.SOUTH, idCache); - - // add edges between the ports - let current: AstNode | undefined = target; - for (let i = 0; current && current?.$type !== "Model"; i++) { - const currentNode = this.idToSNode.get(idCache.getId(current.$container)!); - const edgeType = i === 0 ? STPA_EDGE_TYPE : STPA_INTERMEDIATE_EDGE_TYPE; - currentNode?.children?.push( - this.createSTPAEdge( - idCache.uniqueId(edgeId), - targetPortIds[i + 1], - targetPortIds[i], - children, - edgeType, - getAspect(source) - ) - ); - current = current?.$container; - } - - if (isSystemConstraint(source) && source.$container?.$type !== "Model") { - // if the source is a sub-sytemconstraint we also need intermediate edges to the top system constraint - return this.generateIntermediateOutgoingSTPAEdges( - source, - edgeId, - children, - targetPortIds[targetPortIds.length - 1], - idCache - ); - } else { - // add port for source node - const sourceNode = this.idToSNode.get(sourceId); - const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); - sourceNode?.children?.push(this.createPort(sourcePortId, PortSide.NORTH)); - - // add edge from source to top parent of the target - return this.createSTPAEdge( - edgeId, - sourcePortId, - targetPortIds[targetPortIds.length - 1], - children, - STPA_INTERMEDIATE_EDGE_TYPE, - getAspect(source) - ); - } - } - - /** - * Generates outgoing edges between the {@code source}, its top parent(s), and {@code targetPortId}. - * @param source The source of the original edge. - * @param edgeId The ID of the original edge. - * @param children The children of the original edge. - * @param targetPortId The ID of the target port. - * @param idCache The ID cache of the STPA model. - * @returns the STPAEdge to connect the top parent of the {@code source} with the {@code targetPortId}. - */ - protected generateIntermediateOutgoingSTPAEdges( - source: AstNode, - edgeId: string, - children: SModelElement[], - targetPortId: string, - idCache: IdCache - ): STPAEdge { - // add ports to the source and its (grand)parents - const sourceIds = this.generatePortsForHierarchy(source, edgeId, PortSide.NORTH, idCache); - - // add edges between the ports - let current: AstNode | undefined = source; - for (let i = 0; current && current?.$type !== "Model"; i++) { - const currentNode = this.idToSNode.get(idCache.getId(current.$container)!); - currentNode?.children?.push( - this.createSTPAEdge( - idCache.uniqueId(edgeId), - sourceIds[i], - sourceIds[i + 1], - children, - STPA_INTERMEDIATE_EDGE_TYPE, - getAspect(source) - ) - ); - current = current?.$container; - } - - return this.createSTPAEdge( - edgeId, - sourceIds[sourceIds.length - 1], - targetPortId, - children, - STPA_INTERMEDIATE_EDGE_TYPE, - getAspect(source) - ); - } - - /** - * Generates ports for the {@code current} and its (grand)parents. - * @param current The current node. - * @param edgeId The ID of the original edge for which the ports are created. - * @param side The side of the ports. - * @param idCache The ID cache of the STPA model. - * @returns the IDs of the created ports. - */ - protected generatePortsForHierarchy( - current: AstNode | undefined, - edgeId: string, - side: PortSide, - idCache: IdCache - ): string[] { - const ids: string[] = []; - while (current && current?.$type !== "Model") { - const currentId = idCache.getId(current); - const currentNode = this.idToSNode.get(currentId!); - const portId = idCache.uniqueId(edgeId + "_newTransition"); - currentNode?.children?.push(this.createPort(portId, side)); - ids.push(portId); - current = current?.$container; - } - return ids; - } - - /** - * Generates intermediate edges and ports for the given {@code source} and {@code target} to connect them through hierarchical levels. - * @param source The source of the edge. - * @param target The target of the edge. - * @param edgeId The ID of the original edge. - * @param edgeType The type of the edge. - * @param args The GeneratorContext of the STPA model. - * @param ancestor The common ancestor of the source and target. - * @returns the IDs of the source and target port at the hierarchy level of the {@code ancestor}. - */ - protected generateIntermediateCSEdges( - source: Node | undefined, - target: Node | undefined, - edgeId: string, - edgeType: EdgeType, - args: GeneratorContext, - ancestor?: Node | Graph - ): { sourcePort: string; targetPort: string } { - const assocEdge = { node1: source?.name ?? "", node2: target?.name ?? "" }; - // add ports for source and target and their ancestors till the common ancestor - 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 - ); - // add edges between the ports of the source and its ancestors - for (let i = 0; i < sources.nodes.length - 1; i++) { - const sEdgeType = CS_INTERMEDIATE_EDGE_TYPE; - sources.nodes[i + 1]?.children?.push( - this.createControlStructureEdge( - args.idCache.uniqueId(edgeId), - sources.portIds[i], - sources.portIds[i + 1], - [], - edgeType, - sEdgeType, - args, - false - ) - ); - } - // add edges between the ports of the target and its ancestors - for (let i = 0; i < targets.nodes.length - 1; i++) { - const sEdgeType = i === 0 ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE; - targets.nodes[i + 1]?.children?.push( - this.createControlStructureEdge( - args.idCache.uniqueId(edgeId), - targets.portIds[i + 1], - targets.portIds[i], - [], - edgeType, - sEdgeType, - args, - false - ) - ); - } - // return the source and target port at the hierarchy level of the ancestor - return { - sourcePort: sources.portIds[sources.portIds.length - 1], - targetPort: targets.portIds[targets.portIds.length - 1], - }; - } - - /** - * Adds ports for the {@code current} node and its (grand)parents up to the {@code ancestor}. - * @param current The node for which the ports should be created. - * @param assocEdge The associated edge for which the ports should be created. - * @param edgeId The ID of the original edge for which the ports should be created. - * @param side The side of the ports. - * @param idCache The ID cache of the STPA model. - * @param ancestor The common ancestor of the source and target of the associated edge. - * @returns the IDs of the created ports and the nodes the ports were added to. - */ - protected generatePortsForCSHierarchy( - current: AstNode | undefined, - assocEdge: { node1: string; node2: string }, - edgeId: string, - side: PortSide, - idCache: IdCache, - ancestor?: Node | Graph - ): { portIds: string[]; nodes: SNode[] } { - const ids: string[] = []; - const nodes: SNode[] = []; - while (current && (!ancestor || current !== ancestor)) { - const currentId = idCache.getId(current); - if (currentId) { - const currentNode = this.idToSNode.get(currentId); - if (currentNode) { - // current node could have an invisible child that was skipped while going up the hierarchy because it does not exist in the AST - const invisibleChild = currentNode?.children?.find( - child => child.type === CS_INVISIBLE_SUBCOMPONENT_TYPE - ); - 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, assocEdge)); - ids.push(invisiblePortId); - nodes.push(invisibleChild); - } - // add port for the current node - const nodePortId = idCache.uniqueId(edgeId + "_newTransition"); - currentNode?.children?.push(this.createPort(nodePortId, side, assocEdge)); - ids.push(nodePortId); - nodes.push(currentNode); - current = current?.$container; - } - } - } - return { portIds: ids, nodes: nodes }; - } - - /** - * Creates an STPANode. - * @param node The AstNode for which the STPANode should be created. - * @param nodeId The ID of the STPANode. - * @param lvl The hierarchy level of the STPANode. - * @param children The children of the STPANode. - * @returns an STPANode. - */ - protected createSTPANode( - node: AstNode, - nodeId: string, - lvl: number, - description: string, - children: SModelElement[] - ): STPANode { - return { - type: STPA_NODE_TYPE, - id: nodeId, - aspect: getAspect(node), - description: description, - hierarchyLvl: lvl, - children: children, - layout: "stack", - layoutOptions: { - paddingTop: 10.0, - paddingBottom: 10.0, - paddingLeft: 10.0, - paddingRight: 10.0, - }, - modelOrder: this.options.getModelOrder(), - }; - } - - /** - * Creates an STPAPort. - * @param id The ID of the port. - * @param side The side of the port. - * @returns an STPAPort. - */ - protected createPort(id: string, side: PortSide, assocEdge?: { node1: string; node2: string }): PastaPort { - return { - type: PORT_TYPE, - id: id, - side: side, - associatedEdge: assocEdge, - }; - } - - /** - * Creates an STPAEdge. - * @param id The ID of the edge. - * @param sourceId The ID of the source of the edge. - * @param targetId The ID of the target of the edge. - * @param children The children of the edge. - * @param type The type of the edge. - * @param aspect The aspect of the edge. - * @returns an STPAEdge. - */ - protected createSTPAEdge( - id: string, - sourceId: string, - targetId: string, - children: SModelElement[], - type: string, - aspect: STPAAspect - ): STPAEdge { - return { - type: type, - id: id, - sourceId: sourceId, - targetId: targetId, - children: children, - aspect: aspect, - }; - } - - /** - * Creates a control structure edge based on the given arguments. - * @param edgeId The ID of the edge that should be created. - * @param sourceId The ID of the source of the edge. - * @param targetId The ID of the target of the edge. - * @param label The labels of the edge. - * @param edgeType The type of the edge (control action or feedback edge). - * @param param5 GeneratorContext of the STPA model. - * @returns A control structure edge. - */ - protected createControlStructureEdge( - edgeId: string, - sourceId: string, - targetId: string, - label: string[], - edgeType: EdgeType, - sedgeType: string, - args: GeneratorContext, - dummyLabel: boolean = true - ): CSEdge { - return { - type: sedgeType, - id: edgeId, - sourceId: sourceId!, - targetId: targetId!, - edgeType: edgeType, - children: this.createLabel(label, edgeId, args.idCache, undefined, dummyLabel), - }; - } - - /** - * Generates SLabel elements for the given {@code label}. - * @param label Labels to translate to SLabel elements. - * @param id The ID of the element for which the label should be generated. - * @param idCache The ID cache of the STPA model. - * @param type The type of the label. - * @param dummyLabel Determines whether a dummy label should be created to get a correct layout. - * @returns SLabel elements representing {@code label}. - */ - protected createLabel( - label: string[], - id: string, - idCache: IdCache, - type: string = "label:xref", - dummyLabel: boolean = true - ): SLabel[] { - const children: SLabel[] = []; - if (label.find(l => l !== "")) { - label.forEach(l => { - children.push({ - type: type, - id: idCache.uniqueId(id + "_label"), - text: l, - } as SLabel); - }); - } else if (dummyLabel) { - // needed for correct layout - children.push({ - type: type, - id: idCache.uniqueId(id + "_label"), - text: " ", - } as SLabel); - } - return children; - } - - /** - * Creates a dummy node. - * @param idCache The ID cache of the STPA model. - * @param level The level of the dummy node. - * @returns a dummy node. - */ - protected createDummyNode(name: string, level: number | undefined, idCache: IdCache): CSNode { - const dummyNode: CSNode = { - type: DUMMY_NODE_TYPE, - id: idCache.uniqueId("dummy" + name), - layout: "stack", - layoutOptions: { - paddingTop: 10.0, - paddingBottom: 10.0, - paddngLeft: 10.0, - paddingRight: 10.0, - }, - }; - if (level) { - dummyNode.level = level; - } - return dummyNode; - } - - /** - * Generates the labels for the given node based on {@code showDescription} and the label synthesis options. - * @param showDescription Determines whether the description should be shown. - * @param nodeId The ID of the node for which the labels should be generated. - * @param nodeName The name of the node for which the labels should be generated. - * @param idCache The ID cache of the STPA model. - * @param nodeDescription The description of the node for which the labels should be generated. - * @returns the labels for the given node. - */ - protected generateDescriptionLabels( - showDescription: boolean, - nodeId: string, - nodeName: string, - idCache: IdCache, - nodeDescription?: string - ): SModelElement[] { - let children: SModelElement[] = []; - if (nodeDescription && showDescription) { - children = getDescription( - nodeDescription ?? "", - this.options.getLabelManagement(), - this.options.getLabelShorteningWidth(), - nodeId, - idCache - ); - } - - // show the name in the top line - children.push({ - type: "label", - id: idCache.uniqueId(nodeId + "_label"), - text: nodeName, - }); - return children; - } } diff --git a/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts b/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts new file mode 100644 index 00000000..a046a125 --- /dev/null +++ b/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts @@ -0,0 +1,608 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2024 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { AstNode } from "langium"; +import { GeneratorContext, IdCache } from "langium-sprotty"; +import { SModelElement, SNode } from "sprotty-protocol"; +import { Hazard, Model, SystemConstraint, isContext, isHazard, isSystemConstraint, isUCA } from "../../generated/ast"; +import { collectElementsWithSubComps, leafElement } from "../utils"; +import { createLabel, createPort, createSTPAEdge, createSTPANode, generateDescriptionLabels } from "./diagram-elements"; +import { CustomModel } from "./filtering"; +import { ParentNode, STPAEdge, STPANode } from "./stpa-interfaces"; +import { + PARENT_TYPE, + PortSide, + STPAAspect, + STPA_EDGE_TYPE, + STPA_INTERMEDIATE_EDGE_TYPE, + STPA_NODE_TYPE, +} from "./stpa-model"; +import { StpaSynthesisOptions, showLabelsValue } from "./stpa-synthesis-options"; +import { + createUCAContextDescription, + getAspect, + getAspectsThatShouldHaveDesriptions, + getTargets, + setLevelsForSTPANodes, +} from "./utils"; + +export function createRelationshipGraph( + filteredModel: CustomModel, + model: Model, + idToSNode: Map, + options: StpaSynthesisOptions, + args: GeneratorContext +): ParentNode { + const children = createRelationshipGraphChildren(filteredModel, model, idToSNode, options, args); + + // filtering the nodes of the STPA graph + const stpaNodes: STPANode[] = []; + for (const node of children ?? []) { + if (node.type === STPA_NODE_TYPE) { + stpaNodes.push(node as STPANode); + } + } + // each node should be placed in a specific layer based on the aspect. therefore positions must be set + setLevelsForSTPANodes(stpaNodes, options.getGroupingUCAs()); + + return { + type: PARENT_TYPE, + id: "relationships", + children: children, + modelOrder: options.getModelOrder(), + }; +} + +export function createRelationshipGraphChildren( + filteredModel: CustomModel, + model: Model, + idToSNode: Map, + options: StpaSynthesisOptions, + args: GeneratorContext +): SModelElement[] { + const showLabels = options.getShowLabels(); + // aspects that should have a description when showLabel option is set to automatic + const aspectsToShowDescriptions = getAspectsThatShouldHaveDesriptions(model); + // determine the children for the STPA graph + // for each component a node is generated with edges representing the references of the component + // in order to be able to set the target IDs of the edges, the nodes must be created in the correct order + let stpaChildren: SModelElement[] = filteredModel.losses?.map(l => + generateSTPANode( + l, + showLabels === showLabelsValue.ALL || + showLabels === showLabelsValue.LOSSES || + (showLabels === showLabelsValue.AUTOMATIC && aspectsToShowDescriptions.includes(STPAAspect.LOSS)), + idToSNode, + options, + args + ) + ); + // the hierarchy option determines whether subcomponents are contained in ther parent or not + if (!options.getHierarchy()) { + // subcomponents have edges to the parent + const hazards = collectElementsWithSubComps(filteredModel.hazards); + const sysCons = collectElementsWithSubComps(filteredModel.systemLevelConstraints); + stpaChildren = stpaChildren?.concat([ + ...hazards + .map(hazard => + generateAspectWithEdges( + hazard, + showLabels === showLabelsValue.ALL || + showLabels === showLabelsValue.HAZARDS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.HAZARD)), + idToSNode, + options, + args + ) + ) + .flat(1), + ...sysCons + .map(systemConstraint => + generateAspectWithEdges( + systemConstraint, + showLabels === showLabelsValue.ALL || + showLabels === showLabelsValue.SYSTEM_CONSTRAINTS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.SYSTEMCONSTRAINT)), + idToSNode, + options, + args + ) + ) + .flat(1), + ]); + } else { + // subcomponents are contained in the parent + stpaChildren = stpaChildren?.concat([ + ...filteredModel.hazards + ?.map(hazard => + generateAspectWithEdges( + hazard, + showLabels === showLabelsValue.ALL || + showLabels === showLabelsValue.HAZARDS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.HAZARD)), + idToSNode, + options, + args + ) + ) + .flat(1), + ...filteredModel.systemLevelConstraints + ?.map(systemConstraint => + generateAspectWithEdges( + systemConstraint, + showLabels === showLabelsValue.ALL || + showLabels === showLabelsValue.SYSTEM_CONSTRAINTS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.SYSTEMCONSTRAINT)), + idToSNode, + options, + args + ) + ) + .flat(1), + ...filteredModel.systemLevelConstraints + ?.map(systemConstraint => + systemConstraint.subComponents?.map(subsystemConstraint => + generateEdgesForSTPANode(subsystemConstraint, idToSNode, options, args) + ) + ) + .flat(2), + ]); + } + stpaChildren = stpaChildren?.concat([ + ...filteredModel.responsibilities + ?.map(r => + r.responsiblitiesForOneSystem.map(resp => + generateAspectWithEdges( + resp, + showLabels === showLabelsValue.ALL || + showLabels === showLabelsValue.RESPONSIBILITIES || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.RESPONSIBILITY)), + idToSNode, + options, + args + ) + ) + ) + .flat(2), + ...filteredModel.allUCAs + ?.map(sysUCA => + sysUCA.providingUcas + .concat(sysUCA.notProvidingUcas, sysUCA.wrongTimingUcas, sysUCA.continousUcas) + .map(uca => + generateAspectWithEdges( + uca, + showLabels === showLabelsValue.ALL || + showLabels === showLabelsValue.UCAS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.UCA)), + idToSNode, + options, + args + ) + ) + ) + .flat(2), + ...filteredModel.rules + ?.map(rule => + rule.contexts.map(context => + generateAspectWithEdges( + context, + showLabels === showLabelsValue.ALL || + showLabels === showLabelsValue.UCAS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.UCA)), + idToSNode, + options, + args + ) + ) + ) + .flat(2), + ...filteredModel.controllerConstraints + ?.map(c => + generateAspectWithEdges( + c, + showLabels === showLabelsValue.ALL || + showLabels === showLabelsValue.CONTROLLER_CONSTRAINTS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.CONTROLLERCONSTRAINT)), + idToSNode, + options, + args + ) + ) + .flat(1), + ...filteredModel.scenarios + ?.map(s => + generateAspectWithEdges( + s, + showLabels === showLabelsValue.ALL || + showLabels === showLabelsValue.SCENARIOS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.SCENARIO)), + idToSNode, + options, + args + ) + ) + .flat(1), + ...filteredModel.safetyCons + ?.map(sr => + generateAspectWithEdges( + sr, + showLabels === showLabelsValue.ALL || + showLabels === showLabelsValue.SAFETY_CONSTRAINTS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.SAFETYREQUIREMENT)), + idToSNode, + options, + args + ) + ) + .flat(1), + ]); + return stpaChildren; +} + +/** + * Create the source and target port for the edge with the given {@code edgeId}. + * @param sourceId The id of the source node. + * @param sourceSide The side of the source node the edge should be connected to. + * @param targetId The id of the target node. + * @param targetSide The side of the target node the edge should be connected to. + * @param edgeId The id of the edge. + * @param idCache The id cache of the STPA model. + * @returns the ids of the source and target port the edge should be connected to. + */ +export function createPortsForSTPAEdge( + sourceId: string, + sourceSide: PortSide, + targetId: string, + targetSide: PortSide, + edgeId: string, + idToSNode: Map, + idCache: IdCache +): { sourcePortId: string; targetPortId: string } { + // add ports for source and target + const sourceNode = idToSNode.get(sourceId); + const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); + sourceNode?.children?.push(createPort(sourcePortId, sourceSide)); + + const targetNode = idToSNode.get(targetId!); + const targetPortId = idCache.uniqueId(edgeId + "_newTransition"); + targetNode?.children?.push(createPort(targetPortId, targetSide)); + + return { sourcePortId, targetPortId }; +} + +/** + * Generates a node and the edges for the given {@code node}. + * @param node STPA component for which a node and edges should be generated. + * @param args GeneratorContext of the STPA model. + * @returns A node representing {@code node} and edges representing the references {@code node} contains. + */ +export function generateAspectWithEdges( + node: leafElement, + showDescription: boolean, + idToSNode: Map, + options: StpaSynthesisOptions, + args: GeneratorContext +): SModelElement[] { + // node must be created first in order to access the id when creating the edges + const stpaNode = generateSTPANode(node, showDescription, idToSNode, options, args); + // uca nodes need to save their control action in order to be able to group them by the actions + if ((isUCA(node) || isContext(node)) && node.$container.system.ref) { + stpaNode.controlAction = node.$container.system.ref.name + "." + node.$container.action.ref?.name; + } + const elements: SModelElement[] = generateEdgesForSTPANode(node, idToSNode, options, args); + elements.push(stpaNode); + return elements; +} + +/** + * Generates a single STPANode for the given {@code node}. + * @param node The STPA component the node should be created for. + * @param args GeneratorContext of the STPA model. + * @returns A STPANode representing {@code node}. + */ +export function generateSTPANode( + node: leafElement, + showDescription: boolean, + idToSNode: Map, + options: StpaSynthesisOptions, + args: GeneratorContext +): STPANode { + const idCache = args.idCache; + const nodeId = idCache.uniqueId(node.name, node); + // determines the hierarchy level for subcomponents. For other components the value is 0. + let lvl = 0; + let container = node.$container; + while (isHazard(container) || isSystemConstraint(container)) { + lvl++; + container = container.$container; + } + + let children: SModelElement[] = generateDescriptionLabels( + showDescription, + nodeId, + node.name, + options, + args.idCache, + isContext(node) ? createUCAContextDescription(node) : node.description + ); + // if the hierarchy option is true, the subcomponents are added as children to the parent + if (options.getHierarchy() && isHazard(node) && node.subComponents.length !== 0) { + // adds subhazards + children = children.concat( + node.subComponents?.map((sc: Hazard) => generateSTPANode(sc, showDescription, idToSNode, options, args)) + ); + } + if (options.getHierarchy() && isSystemConstraint(node) && node.subComponents.length !== 0) { + // adds subconstraints + children = children.concat( + node.subComponents?.map((sc: SystemConstraint) => + generateSTPANode(sc, showDescription, idToSNode, options, args) + ) + ); + } + + if (isContext(node)) { + // context UCAs have no description + const result = createSTPANode(node, nodeId, lvl, "", children, options); + idToSNode.set(nodeId, result); + return result; + } else { + const result = createSTPANode(node, nodeId, lvl, node.description, children, options); + idToSNode.set(nodeId, result); + return result; + } +} + +/** + * Generates the edges for {@code node}. + * @param node STPA component for which the edges should be created. + * @param args GeneratorContext of the STPA model. + * @returns Edges representing the references {@code node} contains. + */ +export function generateEdgesForSTPANode( + node: AstNode, + idToSNode: Map, + options: StpaSynthesisOptions, + args: GeneratorContext +): SModelElement[] { + const elements: SModelElement[] = []; + // for every reference an edge is created + // if hierarchy option is false, edges from subcomponents to parents are created too + const targets = getTargets(node, options.getHierarchy()); + for (const target of targets) { + const edge = generateSTPAEdge(node, target, "", idToSNode, args); + if (edge) { + elements.push(edge); + } + } + return elements; +} + +/** + * Generates a single STPAEdge based on the given arguments. + * @param source The source of the edge. + * @param target The target of the edge. + * @param label The label of the edge. + * @param param4 GeneratorContext of the STPA model. + * @returns An STPAEdge. + */ +export function generateSTPAEdge( + source: AstNode, + target: AstNode, + label: string, + idToSNode: Map, + { idCache }: GeneratorContext +): STPAEdge | undefined { + // get the IDs + const targetId = idCache.getId(target); + const sourceId = idCache.getId(source); + const edgeId = idCache.uniqueId(`${sourceId}_${targetId}`, undefined); + + if (sourceId && targetId) { + // create the label of the edge + let children: SModelElement[] = []; + if (label !== "") { + children = createLabel([label], edgeId, idCache); + } + + if ((isHazard(target) || isSystemConstraint(target)) && target.$container?.$type !== "Model") { + // if the target is a subcomponent we need to add several ports and edges through the hierarchical structure + return generateIntermediateIncomingSTPAEdges( + target, + source, + sourceId, + edgeId, + children, + idToSNode, + idCache + ); + } else { + // otherwise it is sufficient to add ports for source and target + const portIds = createPortsForSTPAEdge( + sourceId, + PortSide.NORTH, + targetId, + PortSide.SOUTH, + edgeId, + idToSNode, + idCache + ); + + // add edge between the two ports + return createSTPAEdge( + edgeId, + portIds.sourcePortId, + portIds.targetPortId, + children, + STPA_EDGE_TYPE, + getAspect(source) + ); + } + } +} + +/** + * Generates incoming edges between the {@code source}, the top parent(s), and the {@code target}. + * @param target The target of the edge. + * @param source The source of the edge. + * @param sourceId The ID of the source of the edge. + * @param edgeId The ID of the original edge. + * @param children The children of the original edge. + * @param idCache The ID cache of the STPA model. + * @returns an STPAEdge to connect the {@code source} (or its top parent) with the top parent of the {@code target}. + */ +export function generateIntermediateIncomingSTPAEdges( + target: AstNode, + source: AstNode, + sourceId: string, + edgeId: string, + children: SModelElement[], + idToSNode: Map, + idCache: IdCache +): STPAEdge { + // add ports to the target and its (grand)parents + const targetPortIds = generatePortsForSTPAHierarchy(target, edgeId, PortSide.SOUTH, idToSNode, idCache); + + // add edges between the ports + let current: AstNode | undefined = target; + for (let i = 0; current && current?.$type !== "Model"; i++) { + const currentNode = idToSNode.get(idCache.getId(current.$container)!); + const edgeType = i === 0 ? STPA_EDGE_TYPE : STPA_INTERMEDIATE_EDGE_TYPE; + currentNode?.children?.push( + createSTPAEdge( + idCache.uniqueId(edgeId), + targetPortIds[i + 1], + targetPortIds[i], + children, + edgeType, + getAspect(source) + ) + ); + current = current?.$container; + } + + if (isSystemConstraint(source) && source.$container?.$type !== "Model") { + // if the source is a sub-sytemconstraint we also need intermediate edges to the top system constraint + return generateIntermediateOutgoingSTPAEdges( + source, + edgeId, + children, + targetPortIds[targetPortIds.length - 1], + idToSNode, + idCache + ); + } else { + // add port for source node + const sourceNode = idToSNode.get(sourceId); + const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); + sourceNode?.children?.push(createPort(sourcePortId, PortSide.NORTH)); + + // add edge from source to top parent of the target + return createSTPAEdge( + edgeId, + sourcePortId, + targetPortIds[targetPortIds.length - 1], + children, + STPA_INTERMEDIATE_EDGE_TYPE, + getAspect(source) + ); + } +} + +/** + * Generates outgoing edges between the {@code source}, its top parent(s), and {@code targetPortId}. + * @param source The source of the original edge. + * @param edgeId The ID of the original edge. + * @param children The children of the original edge. + * @param targetPortId The ID of the target port. + * @param idCache The ID cache of the STPA model. + * @returns the STPAEdge to connect the top parent of the {@code source} with the {@code targetPortId}. + */ +export function generateIntermediateOutgoingSTPAEdges( + source: AstNode, + edgeId: string, + children: SModelElement[], + targetPortId: string, + idToSNode: Map, + idCache: IdCache +): STPAEdge { + // add ports to the source and its (grand)parents + const sourceIds = generatePortsForSTPAHierarchy(source, edgeId, PortSide.NORTH, idToSNode, idCache); + + // add edges between the ports + let current: AstNode | undefined = source; + for (let i = 0; current && current?.$type !== "Model"; i++) { + const currentNode = idToSNode.get(idCache.getId(current.$container)!); + currentNode?.children?.push( + createSTPAEdge( + idCache.uniqueId(edgeId), + sourceIds[i], + sourceIds[i + 1], + children, + STPA_INTERMEDIATE_EDGE_TYPE, + getAspect(source) + ) + ); + current = current?.$container; + } + + return createSTPAEdge( + edgeId, + sourceIds[sourceIds.length - 1], + targetPortId, + children, + STPA_INTERMEDIATE_EDGE_TYPE, + getAspect(source) + ); +} + +/** + * Generates ports for the {@code current} and its (grand)parents. + * @param current The current node. + * @param edgeId The ID of the original edge for which the ports are created. + * @param side The side of the ports. + * @param idCache The ID cache of the STPA model. + * @returns the IDs of the created ports. + */ +export function generatePortsForSTPAHierarchy( + current: AstNode | undefined, + edgeId: string, + side: PortSide, + idToSNode: Map, + idCache: IdCache +): string[] { + const ids: string[] = []; + while (current && current?.$type !== "Model") { + const currentId = idCache.getId(current); + const currentNode = idToSNode.get(currentId!); + const portId = idCache.uniqueId(edgeId + "_newTransition"); + currentNode?.children?.push(createPort(portId, side)); + ids.push(portId); + current = current?.$container; + } + return ids; +} From ff1e65a208f991e162a7f71dca1a0653885c780b Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 20 Feb 2024 10:57:53 +0100 Subject: [PATCH 16/16] added comments --- .../stpa/diagram/diagram-controlStructure.ts | 8 ++ .../stpa/diagram/diagram-relationshipGraph.ts | 80 ++++++++++++------- 2 files changed, 57 insertions(+), 31 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts index 6189df36..d70be890 100644 --- a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts +++ b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts @@ -35,6 +35,14 @@ import { import { StpaSynthesisOptions } from "./stpa-synthesis-options"; import { getCommonAncestor, setLevelOfCSNodes, sortPorts } from "./utils"; +/** + * Creates the control structure diagram for the given {@code controlStructure}. + * @param controlStructure The control structure. + * @param idToSNode The map of IDs to SNodes. + * @param options The synthesis options of the STPA model. + * @param args The GeneratorContext of the STPA model. + * @returns the generated control structure diagram. + */ export function createControlStructure( controlStructure: Graph, idToSNode: Map, diff --git a/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts b/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts index a046a125..a6d23457 100644 --- a/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts +++ b/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts @@ -40,6 +40,15 @@ import { setLevelsForSTPANodes, } from "./utils"; +/** + * Creates the relationship graph for the STPA model. + * @param filteredModel The filtered STPA model. + * @param model The STPA model. + * @param idToSNode The map of the generated IDs to their generated SNodes. + * @param options The synthesis options of the STPA model. + * @param args The generator context of the STPA model. + * @returns the relationship graph for the STPA model. + */ export function createRelationshipGraph( filteredModel: CustomModel, model: Model, @@ -67,6 +76,15 @@ export function createRelationshipGraph( }; } +/** + * Creates the children for the relationship graph. + * @param filteredModel The filtered STPA model. + * @param model The STPA model. + * @param idToSNode The map of the generated IDs to their generated SNodes. + * @param options The synthesis options of the STPA model. + * @param args The generator context of the STPA model. + * @returns the children of the relationship graph. + */ export function createRelationshipGraphChildren( filteredModel: CustomModel, model: Model, @@ -263,37 +281,6 @@ export function createRelationshipGraphChildren( return stpaChildren; } -/** - * Create the source and target port for the edge with the given {@code edgeId}. - * @param sourceId The id of the source node. - * @param sourceSide The side of the source node the edge should be connected to. - * @param targetId The id of the target node. - * @param targetSide The side of the target node the edge should be connected to. - * @param edgeId The id of the edge. - * @param idCache The id cache of the STPA model. - * @returns the ids of the source and target port the edge should be connected to. - */ -export function createPortsForSTPAEdge( - sourceId: string, - sourceSide: PortSide, - targetId: string, - targetSide: PortSide, - edgeId: string, - idToSNode: Map, - idCache: IdCache -): { sourcePortId: string; targetPortId: string } { - // add ports for source and target - const sourceNode = idToSNode.get(sourceId); - const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); - sourceNode?.children?.push(createPort(sourcePortId, sourceSide)); - - const targetNode = idToSNode.get(targetId!); - const targetPortId = idCache.uniqueId(edgeId + "_newTransition"); - targetNode?.children?.push(createPort(targetPortId, targetSide)); - - return { sourcePortId, targetPortId }; -} - /** * Generates a node and the edges for the given {@code node}. * @param node STPA component for which a node and edges should be generated. @@ -465,6 +452,37 @@ export function generateSTPAEdge( } } +/** + * Create the source and target port for the edge with the given {@code edgeId}. + * @param sourceId The id of the source node. + * @param sourceSide The side of the source node the edge should be connected to. + * @param targetId The id of the target node. + * @param targetSide The side of the target node the edge should be connected to. + * @param edgeId The id of the edge. + * @param idCache The id cache of the STPA model. + * @returns the ids of the source and target port the edge should be connected to. + */ +export function createPortsForSTPAEdge( + sourceId: string, + sourceSide: PortSide, + targetId: string, + targetSide: PortSide, + edgeId: string, + idToSNode: Map, + idCache: IdCache +): { sourcePortId: string; targetPortId: string } { + // add ports for source and target + const sourceNode = idToSNode.get(sourceId); + const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); + sourceNode?.children?.push(createPort(sourcePortId, sourceSide)); + + const targetNode = idToSNode.get(targetId!); + const targetPortId = idCache.uniqueId(edgeId + "_newTransition"); + targetNode?.children?.push(createPort(targetPortId, targetSide)); + + return { sourcePortId, targetPortId }; +} + /** * Generates incoming edges between the {@code source}, the top parent(s), and the {@code target}. * @param target The target of the edge.