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-controlStructure.ts b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts new file mode 100644 index 00000000..d70be890 --- /dev/null +++ b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts @@ -0,0 +1,497 @@ +/* + * 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"; + +/** + * 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, + 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 726d624b..4e6e9a1b 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 @@ -15,49 +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, - Hazard, - Model, - Node, - SystemConstraint, - 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, STPAEdge, STPANode, STPAPort } from "./stpa-interfaces"; -import { - CS_EDGE_TYPE, - CS_NODE_TYPE, - DUMMY_NODE_TYPE, - EdgeType, - PARENT_TYPE, - PortSide, - STPAAspect, - STPA_EDGE_TYPE, - STPA_INTERMEDIATE_EDGE_TYPE, - STPA_NODE_TYPE, - STPA_PORT_TYPE, -} from "./stpa-model"; -import { StpaSynthesisOptions, showLabelsValue } from "./stpa-synthesis-options"; -import { - createUCAContextDescription, - getAspect, - getAspectsThatShouldHaveDesriptions, - getTargets, - setLevelOfCSNodes, - setLevelsForSTPANodes, -} from "./utils"; +import { StpaSynthesisOptions } from "./stpa-synthesis-options"; export class StpaDiagramGenerator extends LangiumDiagramGenerator { protected readonly options: StpaSynthesisOptions; @@ -81,204 +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((sh) => - this.generateAspectWithEdges( - sh, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.HAZARDS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.HAZARD)), - args - ) - ) - .flat(1), - ...sysCons - .map((ssc) => - this.generateAspectWithEdges( - ssc, - 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((h) => - this.generateAspectWithEdges( - h, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.HAZARDS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.HAZARD)), - args - ) - ) - .flat(1), - ...filteredModel.systemLevelConstraints - ?.map((sc) => - this.generateAspectWithEdges( - sc, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.SYSTEM_CONSTRAINTS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.SYSTEMCONSTRAINT)), - args - ) - ) - .flat(1), - ...filteredModel.systemLevelConstraints - ?.map((sc) => sc.subComponents?.map((ssc) => this.generateEdgesForSTPANode(ssc, 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) - ]; // 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", @@ -286,688 +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, { idCache }: GeneratorContext): CSNode { - const label = node.label ? node.label : node.name; - const nodeId = idCache.uniqueId(node.name, node); - const csNode = { - type: CS_NODE_TYPE, - id: nodeId, - level: node.level, - children: this.createLabel([label], nodeId, idCache), - layout: "stack", - layoutOptions: { - paddingTop: 10.0, - paddingBottom: 10.0, - paddingLeft: 10.0, - paddingRight: 10.0, - }, - }; - this.idToSNode.set(nodeId, csNode); - return csNode; - } - - /** - * 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)); - } - return edges; - } - - /** - * 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 args GeneratorContext of the STPA model. - * @returns A list of edges representing the commands. - */ - protected translateCommandsToEdges( - commands: VerticalEdge[], - 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 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); - } - 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.createSTPAPort(sourcePortId, sourceSide)); - - const targetNode = this.idToSNode.get(targetId!); - const targetPortId = idCache.uniqueId(edgeId + "_newTransition"); - targetNode?.children?.push(this.createSTPAPort(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. - */ - 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, - 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, - args - ); - graphComponents = [outputEdge, outputDummyNode]; - break; - default: - console.error("EdgeType is not INPUT or OUTPUT"); - break; - } - return 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.generateIntermediateIncomingEdges(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 generateIntermediateIncomingEdges( - 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.generateIntermediateOutgoingEdges( - 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.createSTPAPort(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 generateIntermediateOutgoingEdges( - 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.createSTPAPort(portId, side)); - ids.push(portId); - current = current?.$container; - } - return ids; - } - - /** - * 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 createSTPAPort(id: string, side: PortSide): STPAPort { - return { - type: STPA_PORT_TYPE, - id: id, - side: side, - }; - } - - /** - * 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, - args: GeneratorContext - ): CSEdge { - return { - type: CS_EDGE_TYPE, - id: edgeId, - sourceId: sourceId!, - targetId: targetId!, - edgeType: edgeType, - children: this.createLabel(label, edgeId, args.idCache), - }; - } - - /** - * 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. - * @returns SLabel elements representing {@code label}. - */ - protected createLabel(label: string[], id: string, idCache: IdCache): SLabel[] { - const children: SLabel[] = []; - if (label.find((l) => l !== "")) { - label.forEach((l) => { - children.push({ - type: "label:xref", - id: idCache.uniqueId(id + "_label"), - text: l, - } as SLabel); - }); - } else { - // needed for correct layout - children.push({ - type: "label:xref", - 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..a6d23457 --- /dev/null +++ b/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts @@ -0,0 +1,626 @@ +/* + * 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"; + +/** + * 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, + 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(), + }; +} + +/** + * 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, + 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; +} + +/** + * 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) + ); + } + } +} + +/** + * 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. + * @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; +} diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index 7717d970..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 @@ -16,20 +16,26 @@ */ /* 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, PastaPort } from "./stpa-interfaces"; +import { + CS_NODE_TYPE, + CS_INVISIBLE_SUBCOMPONENT_TYPE, + PARENT_TYPE, + PROCESS_MODEL_PARENT_NODE_TYPE, + PortSide, + STPA_NODE_TYPE, + 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 +43,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 +83,10 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { switch (snode.type) { case CS_NODE_TYPE: return this.csNodeOptions(snode as CSNode); + 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: @@ -84,76 +94,123 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { } } + /** + * 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", + // TODO: wait for node size fix in elkjs + // "org.eclipse.elk.algorithm": "rectpacking", + }; + } + + /** + * 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", + "org.eclipse.elk.portConstraints": "FIXED_SIDE", + "org.eclipse.elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES", + "org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true" + }; + } + + /** + * Options for the standard STPA nodes. + */ protected stpaNodeOptions(node: STPANode): LayoutOptions { - if (node.children?.find(child => child.type.startsWith('node'))) { + if (node.children?.find(child => child.type.startsWith("node"))) { + // node has further children nodes 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", }; } } + /** + * 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 = { - '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; } + /** + * Options for a standard control structure node. + */ protected csNodeOptions(node: CSNode): LayoutOptions { - return { - 'org.eclipse.elk.nodeLabels.placement': "INSIDE V_CENTER H_CENTER", - 'org.eclipse.elk.partitioning.partition': "" + node.level, - 'org.eclipse.elk.nodeSize.constraints': 'NODE_LABELS', + 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', + "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"))) { + // 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: 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 === STPA_PORT_TYPE) { - let side = ''; - switch ((sport as STPAPort).side) { + if (sport.type === PORT_TYPE) { + let side = ""; + switch ((sport as PastaPort).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-interfaces.ts b/extension/src-language-server/stpa/diagram/stpa-interfaces.ts index e2dba7ae..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,39 +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 STPAPort extends SPort { - side?: PortSide +export interface PastaPort extends SPort { + 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 ae461b04..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,12 +19,16 @@ export const STPA_NODE_TYPE = 'node:stpa'; export const PARENT_TYPE= 'node:parent'; export const CS_NODE_TYPE = 'node:cs'; +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 STPA_PORT_TYPE = 'port:stpa'; +export const PORT_TYPE = 'port:pasta'; +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..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"; @@ -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 process model of controllers. + */ +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); } @@ -386,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: @@ -417,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; @@ -495,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 d9b819cd..e5ea7b19 100644 --- a/extension/src-language-server/stpa/diagram/utils.ts +++ b/extension/src-language-server/stpa/diagram/utils.ts @@ -16,8 +16,10 @@ */ import { AstNode } from "langium"; +import { SModelElement } from "sprotty-protocol"; import { Context, + Graph, Model, Node, isActionUCAs, @@ -33,7 +35,7 @@ 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"; @@ -69,7 +71,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,20 +180,26 @@ export function setLevelOfCSNodes(nodes: Node[]): void { const visited = new Map>(); for (const node of nodes) { visited.set(node.name, new Set()); + if (node.children) { + // set levels of children seperately + setLevelOfCSNodes(node.children); + } + } + if (nodes.length > 0) { + nodes[0].level = 0; + assignLevel(nodes[0], visited); } - nodes[0].level = 0; - assignLevel(nodes[0], visited); } /** - * 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. */ 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 (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; @@ -201,7 +209,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 (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; @@ -312,7 +320,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; } @@ -357,3 +365,78 @@ export function getAspectsThatShouldHaveDesriptions(model: Model): STPAAspect[] } return aspectsToShowDescriptions; } + +/** + * 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; + while (current?.$type !== "Graph") { + ancestors.push(current.$container); + current = current.$container; + } + 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 => { + if (child.type.startsWith("port")) { + ports.push(child as any as PastaPort); + } else { + otherChildren.push(child); + } + }); + + // sort the ports based on their associated edges + const newPorts: PastaPort[] = []; + for (const port of ports) { + newPorts.push(port); + if (port.associatedEdge) { + for (const otherPort of ports) { + if ( + 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); + } + } + } + } + + 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-language-server/stpa/stpa-scopeProvider.ts b/extension/src-language-server/stpa/stpa-scopeProvider.ts index 8779c687..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 @@ -25,14 +25,43 @@ 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 } from "../generated/ast"; +import { + ActionUCAs, + Command, + Context, + DCAContext, + DCARule, + Graph, + Hazard, + LossScenario, + Model, + Node, + Rule, + SystemConstraint, + UCA, + Variable, + VerticalEdge, + isActionUCAs, + isContext, + isControllerConstraint, + isDCAContext, + isDCARule, + isGraph, + isHazardList, + isLossScenario, + isModel, + isResponsibility, + isRule, + isSafetyConstraint, + isSystemConstraint, + isSystemResponsibilities, + isVerticalEdge, +} 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; @@ -40,6 +69,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); @@ -56,9 +86,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); @@ -68,6 +106,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); } @@ -86,7 +126,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; } @@ -129,6 +172,40 @@ 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)) { + graph = graph.$container; + } + + const allDescriptions = this.getChildrenNodes(graph.nodes, precomputed); + return this.descriptionsToScope(allDescriptions); + } + + /** + * 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; + 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. @@ -167,7 +244,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); } @@ -178,7 +259,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) { @@ -222,7 +307,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); @@ -248,5 +337,4 @@ export class StpaScopeProvider extends DefaultScopeProvider { } return result; } - } 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/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..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"; @@ -53,21 +71,27 @@ import { CSEdge, CSNode, CS_EDGE_TYPE, + CS_INTERMEDIATE_EDGE_TYPE, + CS_INVISIBLE_SUBCOMPONENT_TYPE, CS_NODE_TYPE, DUMMY_NODE_TYPE, + HEADER_LABEL_TYPE, PARENT_TYPE, + PORT_TYPE, + PROCESS_MODEL_PARENT_NODE_TYPE, + PastaPort, STPAEdge, STPANode, - STPAPort, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE, STPA_NODE_TYPE, - STPA_PORT_TYPE, } from "./stpa/stpa-model"; import { StpaMouseListener } from "./stpa/stpa-mouselistener"; import { CSNodeView, + HeaderLabelView, IntermediateEdgeView, + InvisibleNodeView, PolylineArrowEdgeView, PortView, STPAGraphView, @@ -89,26 +113,30 @@ 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 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); // STPA configureModelElement(context, "graph", SGraph, STPAGraphView); + 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); 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); + configureModelElement(context, PORT_TYPE, PastaPort, PortView); // FTA configureModelElement(context, FTA_EDGE_TYPE, FTAEdge, PolylineArrowEdgeViewFTA); @@ -121,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/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 1ef0c53b..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,12 +21,16 @@ 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 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 STPA_EDGE_TYPE = 'edge:stpa'; export const STPA_INTERMEDIATE_EDGE_TYPE = 'edge:stpa-intermediate'; -export const STPA_PORT_TYPE = 'port:stpa'; +export const CS_INTERMEDIATE_EDGE_TYPE = 'edge:cs-intermediate'; +export const PORT_TYPE = 'port:pasta'; +export const HEADER_LABEL_TYPE = 'label:header'; export class ParentNode extends SNode { modelOrder: boolean; @@ -36,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 = ""; @@ -58,8 +61,10 @@ 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; + /** 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 }; } /** @@ -67,9 +72,7 @@ export class STPAPort 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]; } /** @@ -92,7 +95,7 @@ export enum STPAAspect { CONTROLLERCONSTRAINT, SCENARIO, SAFETYREQUIREMENT, - UNDEFINED + UNDEFINED, } /** @@ -103,7 +106,7 @@ export enum EdgeType { FEEDBACK, INPUT, OUTPUT, - UNDEFINED + UNDEFINED, } /** Possible sides for a port. */ @@ -111,5 +114,5 @@ export enum PortSide { WEST, EAST, NORTH, - SOUTH -} \ No newline at end of file + SOUTH, +} diff --git a/extension/src-webview/stpa/stpa-views.tsx b/extension/src-webview/stpa/stpa-views.tsx index 4b70a21a..ae89436a 100644 --- a/extension/src-webview/stpa/stpa-views.tsx +++ b/extension/src-webview/stpa/stpa-views.tsx @@ -18,12 +18,12 @@ /** @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'; +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"; @@ -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 { @@ -238,4 +250,13 @@ export class PortView implements IView { render(model: SPort, context: RenderingContext): VNode { return ; } -} \ No newline at end of file +} + +@injectable() +export class HeaderLabelView extends SLabelView { + render(label: Readonly, context: RenderingContext): VNode | undefined { + return + {super.render(label, context)} + + } +}