diff --git a/extension/README.md b/extension/README.md index dbf753c..076c3e9 100644 --- a/extension/README.md +++ b/extension/README.md @@ -109,7 +109,6 @@ SC1 "Vessel must not be exposed to major damage or breakdown" [H1] { ControlStructure Ferry { ControlCentre { - hierarchyLevel 0 processModel { mode: [docking, driving] } @@ -121,7 +120,6 @@ Ferry { } } VirtualCaptain { - hierarchyLevel 1 controlActions { [pars "Set parameters"] -> Engine } @@ -130,7 +128,6 @@ Ferry { } } Engine { - hierarchyLevel 2 feedback { [motion "Motion"] -> VirtualCaptain } @@ -177,6 +174,8 @@ RL1 { } } ``` +*Note* +The keyword `hierarchyLevel` is deprecated and is no longer supported. The arrangement of the nodes in the different levels of the relationship graph is now determined using the model order. ### FTA diff --git a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts index 581dfaf..3f56021 100644 --- a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts +++ b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts @@ -21,7 +21,7 @@ import { SModelElement, SLabel, SNode, HAlignment } from "sprotty-protocol"; import { expansionState } from "../../diagram-server.js"; import { Command, Graph, Node, Variable, VerticalEdge } from "../../generated/ast.js"; import { createControlStructureEdge, createInvisibleProcessModelEdge, createDummyNode, createLabel, createPort } from "./diagram-elements.js"; -import { CSEdge, CSNode, ParentNode } from "./stpa-interfaces.js"; +import { CSEdge, CSNode, ParentNode, PastaPort } from "./stpa-interfaces.js"; import { CS_EDGE_TYPE, CS_INTERMEDIATE_EDGE_TYPE, @@ -37,6 +37,9 @@ import { import { StpaSynthesisOptions } from "./stpa-synthesis-options.js"; import { getCommonAncestor, sortPorts } from "./utils.js"; +const inputID = "input" as const; +const outputID = "output" as const; + /** * Creates the control structure diagram for the given {@code controlStructure}. * @param controlStructure The control structure. @@ -61,11 +64,24 @@ export function createControlStructure( // setLevelOfCSNodes(controlStructure.nodes); // determine the nodes of the control structure graph const csNodes = controlStructure.nodes.map(n => createControlStructureNode(n, idToSNode, options, idCache)); - // children (nodes and edges) of the control structure + + // get all edges of graph (plus dummyNodes for input/output) + const verticalEdgesAndDummyNodes = generateVerticalCSEdges(controlStructure.nodes, idToSNode, idCache, missingReferences, addMissing, missingFeedback); + const dummyNodes = verticalEdgesAndDummyNodes.filter(item => item.type === "node:dummy"); + const verticalEdges = verticalEdgesAndDummyNodes.filter(item => item.type !== "node:dummy"); + + // children of the control structure (first input dummys, then nodes / edges, last output dummys) const CSChildren = [ + ...csNodes.flatMap(csNode => { + const inputDummy = dummyNodes.find(d => d.id === `dummy${inputID}${csNode.id}`); + return inputDummy ? [inputDummy] : []; + }), ...csNodes, - ...generateVerticalCSEdges(controlStructure.nodes, idToSNode, idCache, missingReferences, addMissing, missingFeedback), - //...this.generateHorizontalCSEdges(filteredModel.controlStructure.edges, idCache) + ...csNodes.flatMap(csNode => { + const outputDummy = dummyNodes.find(d => d.id === `dummy${outputID}${csNode.id}`); + return outputDummy ? [outputDummy] : []; + }), + ...verticalEdges, ]; // 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[]); @@ -358,11 +374,49 @@ export function createEdgeForCommand( } } +/** + * Creates input / output ports for a given node. + * @param nodeId The ID of the node. + * @param idToSNode The map of IDs to SNodes. + * @param portTypes Shows if port should be input or output + * @param assocEdge The associated edge for which the ports should be created. + */ +function addPortsToNode( + nodeId: string, + idToSNode: Map, + portTypes: typeof inputID | typeof outputID, + assocEdge: { node1: string; node2: string } +): void { + const csNode = idToSNode.get(nodeId); + if (csNode) { + // Initialize children array if it doesn't exist + if (!csNode.children) { + csNode.children = []; + } + + if (portTypes === inputID) { + const inputPortId = csNode.id + `_port_${inputID}`; + // Check if port already exists + if (!csNode.children.find((child: PastaPort) => child.id === inputPortId)) { + csNode.children.push(createPort(inputPortId, PortSide.WEST, assocEdge)); + } + } + if (portTypes === outputID) { + const outputPortId = csNode.id + `_port_${outputID}`; + // Check if port already exists + if (!csNode.children.find((child: PastaPort) => child.id === outputPortId)) { + csNode.children.push(createPort(outputPortId, PortSide.EAST, assocEdge)); + } + } + } +} + /** * 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 idToSNode The map of IDs to SNodes. * @param idCache The ID cache of the STPA model. * @returns a list of edges representing the inputs or outputs that should be added at the top level. */ @@ -386,17 +440,25 @@ export function translateIOToEdgeAndNode( let graphComponents: (CSNode | CSEdge)[] = []; switch (edgetype) { case EdgeType.INPUT: + // create input port for node // create dummy node for the input const inputDummyNode = createDummyNode( - "input" + node.name, + inputID + node.name, node.level ? node.level - 1 : undefined, - idCache + idCache, + nodeId ?? "" ); + const assocEdgeInput = { node1: inputDummyNode.id ?? "", node2: nodeId ?? "" }; + if (nodeId) { + addPortsToNode(nodeId, idToSNode, inputID, assocEdgeInput); + } else { + console.error("No node with ID in cache"); + } // create edge for the input const inputEdge = createControlStructureEdge( - idCache.uniqueId(`${inputDummyNode.id}_input_${nodeId}`), - inputDummyNode.id ? inputDummyNode.id : "", - nodeId ? nodeId : "", + idCache.uniqueId(`${inputDummyNode.id}_${inputID}_${nodeId}`), + inputDummyNode.id ? inputDummyNode.id + "_port" : "", + nodeId ? nodeId + `_port_${inputID}` : "", label, edgetype, CS_EDGE_TYPE, @@ -407,15 +469,24 @@ export function translateIOToEdgeAndNode( case EdgeType.OUTPUT: // create dummy node for the output const outputDummyNode = createDummyNode( - "output" + node.name, + outputID + node.name, node.level ? node.level + 1 : undefined, - idCache + idCache, + nodeId ?? "" ); + const assocEdgeOutput = { node1: nodeId ?? "", node2: outputDummyNode.id ?? "" }; + // create output port for node + if (nodeId) { + addPortsToNode(nodeId, idToSNode, outputID, assocEdgeOutput); + } else { + console.error("No node with ID in cache"); + } + // create edge for the output const outputEdge = createControlStructureEdge( - idCache.uniqueId(`${nodeId}_output_${outputDummyNode.id}`), - nodeId ? nodeId : "", - outputDummyNode.id ? outputDummyNode.id : "", + idCache.uniqueId(`${nodeId}_${outputID}_${outputDummyNode.id}`), + nodeId ? nodeId + `_port_${outputID}` : "", + outputDummyNode.id ? outputDummyNode.id + "_port" : "", label, edgetype, CS_EDGE_TYPE, diff --git a/extension/src-language-server/stpa/diagram/diagram-elements.ts b/extension/src-language-server/stpa/diagram/diagram-elements.ts index d8d7620..7820c62 100644 --- a/extension/src-language-server/stpa/diagram/diagram-elements.ts +++ b/extension/src-language-server/stpa/diagram/diagram-elements.ts @@ -235,14 +235,21 @@ export function createLabel( * Creates a dummy node. * @param idCache The ID cache of the STPA model. * @param level The level of the dummy node. + * @param assocNode The associated node of the input / output. (Is either source or target of the connected edge.) * @returns a dummy node. */ -export function createDummyNode(name: string, level: number | undefined, idCache: IdCache): CSNode { +export function createDummyNode(name: string, level: number | undefined, idCache: IdCache, assocNode: string): CSNode { + const isInput = name.startsWith("input"); + const assocEdge = { + node1: isInput ? name : assocNode, + node2: isInput ? assocNode : name + }; const dummyNode: CSNode = { type: DUMMY_NODE_TYPE, id: idCache.uniqueId("dummy" + name), layout: "stack", - hasChildren: false, + hasChildren: true, + children: [createPort(idCache.uniqueId("dummy" + name + "_port"), isInput ? PortSide.SOUTH : PortSide.NORTH, assocEdge)], expanded: false, layoutOptions: { paddingTop: 10.0, diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index 9c44dbc..b6aaef4 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -200,7 +200,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { // 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.layered.considerModelOrder.strategy": "NODES_AND_EDGES", + "org.eclipse.elk.layered.considerModelOrder.strategy": "PREFER_NODES", "org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true", "org.eclipse.elk.layered.cycleBreaking.strategy": "MODEL_ORDER", "org.eclipse.elk.separateConnectedComponents": "false"