diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index e34e9c8..2204b42 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 7923f10..aa1f5fd 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 e3522a5..6408a5d 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 f8d6ec4..d541352 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 129b4fd..148af50 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 40c9df6..3e8c319 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 0914cc9..c96a8af 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 b397cbc..1135133 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 7954dd1..566e6f6 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]; }