From df7aa3fcd5dbee1b0c0f497b2848b6359f300011 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 24 Oct 2023 10:25:14 +0200 Subject: [PATCH 01/33] causal factors for scenarios can be stated --- extension/src-language-server/stpa/stpa.langium | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/extension/src-language-server/stpa/stpa.langium b/extension/src-language-server/stpa/stpa.langium index 63a8c5c5..e54c4306 100644 --- a/extension/src-language-server/stpa/stpa.langium +++ b/extension/src-language-server/stpa/stpa.langium @@ -127,8 +127,14 @@ ControllerConstraint: name=ID description=STRING '['refs+=[UCA] (',' refs+=[UCA])*']'; LossScenario: - (name=ID description=STRING list=HazardList) | - (name=ID 'for' uca=[UCA] description=STRING (list=HazardList)?); + (name=ID ('<' factor=CausalFactor '>')? description=STRING list=HazardList) | + (name=ID ('<' factor=CausalFactor '>')? 'for' uca=[UCA] description=STRING (list=HazardList)?); + +CausalFactor returns string: + 'controlAction' | 'inadequateOperation' | 'delayedOperation' | 'componentFailure' | 'changesOverTime' | + 'conflictingCA' | 'wrongProcessInput' | 'disturbance' | 'processOutput' | 'delayedFeedback' | + 'measurementInaccurate' | 'incorrectInformationProvided' | 'missingFeedback' | 'inadequateControlAlgorithm' | + 'wrongProcessModel' | 'wrongControlInput'; HazardList: '[' refs+=[Hazard:SubID] (',' refs+=[Hazard:SubID])*']'; From b5f8757b28b04564a6a879839fead083181549e2 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 24 Oct 2023 10:25:50 +0200 Subject: [PATCH 02/33] added process for creating fault trees from stpa --- extension/package.json | 30 +++++---- .../fta/analysis/fta-cutSet-calculator.ts | 6 +- .../src-language-server/fta/diagram/utils.ts | 5 +- extension/src-language-server/fta/fta.langium | 2 +- extension/src-language-server/fta/utils.ts | 47 ++++++++++--- .../stpa/ftaGeneration/fta-generation.ts | 66 +++++++++++++++++++ .../stpa/message-handler.ts | 16 +++++ extension/src/extension.ts | 13 +++- 8 files changed, 158 insertions(+), 27 deletions(-) create mode 100644 extension/src-language-server/stpa/ftaGeneration/fta-generation.ts diff --git a/extension/package.json b/extension/package.json index 223346b9..0d8dd8c1 100644 --- a/extension/package.json +++ b/extension/package.json @@ -172,6 +172,11 @@ "title": "Create a Markdown file", "category": "STPA PDF Creation" }, + { + "command": "pasta.stpa.ft.generation", + "title": "Generate Fault Trees", + "category": "STPA and FTA" + }, { "command": "pasta.fta.cutSets", "title": "Generate the cut sets", @@ -237,6 +242,10 @@ "command": "pasta.stpa.md.creation", "when": "editorLangId == 'stpa'" }, + { + "command": "pasta.stpa.ft.generation", + "when": "editorLangId == 'stpa'" + }, { "command": "pasta.fta.cutSets", "when": "editorLangId == 'fta'" @@ -262,7 +271,7 @@ "group": "checks" }, { - "submenu": "pasta.generate", + "submenu": "pasta.fta.generate", "group": "generate" }, { @@ -274,6 +283,11 @@ "command": "pasta.stpa.md.creation", "when": "editorLangId == 'stpa'", "group": "stpa" + }, + { + "command": "pasta.stpa.ft.generation", + "when": "editorLangId == 'stpa'", + "group": "stpa" } ], "pasta.stpa.checks": [ @@ -298,7 +312,7 @@ "group": "navigation" } ], - "pasta.generate": [ + "pasta.fta.generate": [ { "command": "pasta.fta.cutSets", "when": "editorLangId == 'fta'", @@ -332,16 +346,6 @@ "command": "pasta.contextTable.open", "when": "resourceExtname == '.stpa'", "group": "navigation" - }, - { - "command": "pasta.fta.cutSets", - "when": "resourceExtname == '.fta'", - "group": "navigation" - }, - { - "command": "pasta.fta.minimalCutSets", - "when": "resourceExtname == '.fta'", - "group": "navigation" } ] }, @@ -351,7 +355,7 @@ "label": "Validation Checks" }, { - "id": "pasta.generate", + "id": "pasta.fta.generate", "label": "Generate cut sets" } ], diff --git a/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts b/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts index c103cb5c..d76ca19b 100644 --- a/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts +++ b/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts @@ -213,9 +213,9 @@ function getChildrenOfNode(node: AstNode): namedFtaElement[] { * @returns the child of the top event. */ function getChildOfTopEvent(allNodes: AstNode[]): namedFtaElement | undefined { - const topEventChildren = (allNodes.find((node) => isTopEvent(node)) as TopEvent).children; - if (topEventChildren.length !== 0) { - return topEventChildren[0].ref; + const topEventChild = (allNodes.find((node) => isTopEvent(node)) as TopEvent).child; + if (topEventChild) { + return topEventChild.ref; } } diff --git a/extension/src-language-server/fta/diagram/utils.ts b/extension/src-language-server/fta/diagram/utils.ts index 7cd51459..71d008a0 100644 --- a/extension/src-language-server/fta/diagram/utils.ts +++ b/extension/src-language-server/fta/diagram/utils.ts @@ -60,7 +60,10 @@ export function getFTNodeType(node: AstNode): FTNodeType { export function getTargets(node: AstNode): AstNode[] { const targets: AstNode[] = []; // only the top event and gates have children - if (isTopEvent(node) || isGate(node)) { + if (isTopEvent(node) && node.child.ref) { + targets.push(node.child.ref); + } + if (isGate(node)) { for (const child of node.children) { if (child.ref) { targets.push(child.ref); diff --git a/extension/src-language-server/fta/fta.langium b/extension/src-language-server/fta/fta.langium index 40ae3944..02e7f0db 100644 --- a/extension/src-language-server/fta/fta.langium +++ b/extension/src-language-server/fta/fta.langium @@ -16,7 +16,7 @@ Gate: AND|OR|KNGate|InhibitGate; TopEvent: - name=STRING "=" children+=[Children:ID]; + name=STRING "=" child=[Children:ID]; Children: Gate | Component; diff --git a/extension/src-language-server/fta/utils.ts b/extension/src-language-server/fta/utils.ts index 938a680b..39ad886c 100644 --- a/extension/src-language-server/fta/utils.ts +++ b/extension/src-language-server/fta/utils.ts @@ -16,7 +16,7 @@ */ import { Range } from "vscode-languageserver"; -import { Component, Condition, Gate, ModelFTA, TopEvent } from "../generated/ast"; +import { Component, Condition, Gate, ModelFTA, TopEvent, isAND, isInhibitGate, isKNGate, isOR } from "../generated/ast"; export type namedFtaElement = Component | Condition | Gate | TopEvent; @@ -43,12 +43,7 @@ export function cutSetsToString(cutSets: Set[]): string[] { */ export function getRangeOfNodeFTA(model: ModelFTA, label: string): Range | undefined { let range: Range | undefined = undefined; - const elements: namedFtaElement[] = [ - model.topEvent, - ...model.components, - ...model.conditions, - ...model.gates - ]; + const elements: namedFtaElement[] = [model.topEvent, ...model.components, ...model.conditions, ...model.gates]; elements.forEach((component) => { if (component.name === label) { range = component.$cstNode?.range; @@ -56,4 +51,40 @@ export function getRangeOfNodeFTA(model: ModelFTA, label: string): Range | undef } }); return range; -} \ No newline at end of file +} + +/** + * Serializes an FTA AST. + * @param model The model of the FTA AST to serialize. + * @returns the result of the serialization. + */ +export function serializeFTAAST(model: ModelFTA): string { + let result = ""; + if (model.components && model.components.length !== 0) { + result += "Components\n"; + model.components.forEach((component) => (result += `${component.name} "${component.description}"\n`)); + } + if (model.conditions && model.conditions.length !== 0) { + result += "Conditions\n"; + model.conditions.forEach((condition) => (result += `${condition.name} "${condition.description}"\n`)); + } + if (model.topEvent) { + result += "TopEvent\n"; + result += `"${model.topEvent.name}" = ${model.topEvent.child.$refText}\n`; + } + if (model.gates && model.gates.length !== 0) { + result += "Gates\n"; + model.gates.forEach((gate) => { + if (isAND(gate)) { + result += `${gate.name} = ${gate.children.join(" and ")}\n`; + } else if (isOR(gate)) { + result += `${gate.name} = ${gate.children.join(" or ")}\n`; + } else if (isKNGate(gate)) { + result += `${gate.name} = ${gate.k} of ${gate.children.join(", ")}\n`; + } else if (isInhibitGate(gate)) { + result += `${gate.name} = ${gate.condition} inhibits ${gate.children.join("")}\n`; + } + }); + } + return result; +} diff --git a/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts b/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts new file mode 100644 index 00000000..00a43de3 --- /dev/null +++ b/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts @@ -0,0 +1,66 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 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 { LangiumSprottySharedServices } from "langium-sprotty"; +import { Component, Hazard, Model, ModelFTA } from "../../generated/ast"; +import { getModel } from "../../utils"; + +/** + * Create the fault trees for an stpa model as ASTs + * @param uri The uri of the stpa file for which the fault trees should be created. + * @param shared The shared services of Langium and Sprotty. + * @returns the ASTs of the created fault trees. + */ +export async function createFaultTrees(uri: string, shared: LangiumSprottySharedServices): Promise { + // get the current model + const model = (await getModel(uri, shared)) as Model; + const faultTrees: ModelFTA[] = []; + for (const hazard of model.hazards) { + faultTrees.push(createFaulTreeForHazard(model, hazard)); + } + return faultTrees; +} + +/** + * Creates a fault tree with {@code hazard} as top event. + * @param stpaModel The stpa model that contains the {@code hazard}. + * @param hazard The hazard for which the fault tree should be created. + * @returns the AST of the created fault tree with {@code hazard} as top event. + */ +function createFaulTreeForHazard(stpaModel: Model, hazard: Hazard): ModelFTA { + const ftaModel = {} as ModelFTA; + ftaModel.components = []; + ftaModel.gates = []; + + const component = { name: hazard.name, description: hazard.description } as Component; + ftaModel.components.push(component); + + const scenarios = stpaModel.scenarios.filter((scenario) => { + if (scenario.list?.refs?.find((ref) => ref.$refText === hazard.name) !== undefined) { + return true; + } else { + return false; + } + }); + for (const scenario of scenarios) { + const component = { name: scenario.name, description: scenario.description } as Component; + ftaModel.components.push(component); + } + //TODO: implement + + return ftaModel; +} diff --git a/extension/src-language-server/stpa/message-handler.ts b/extension/src-language-server/stpa/message-handler.ts index 71270194..8f5094a8 100644 --- a/extension/src-language-server/stpa/message-handler.ts +++ b/extension/src-language-server/stpa/message-handler.ts @@ -20,6 +20,8 @@ import { LangiumSprottySharedServices } from "langium-sprotty"; import { TextDocumentContentChangeEvent } from "vscode"; import { Connection, URI } from "vscode-languageserver"; import { diagramSizes } from "../diagram-server"; +import { serializeFTAAST } from "../fta/utils"; +import { createFaultTrees } from "./ftaGeneration/fta-generation"; import { generateLTLFormulae } from "./modelChecking/model-checking"; import { createResultData } from "./result-report/result-generator"; import { StpaServices } from "./stpa-module"; @@ -46,6 +48,7 @@ export function addSTPANotificationHandler( addTextChangeHandler(connection, stpaServices, sharedServices); addVerificationHandler(connection, sharedServices); addResultHandler(connection, sharedServices); + addFTAGeneratorHandler(connection, sharedServices); } /** @@ -153,3 +156,16 @@ function addResultHandler(connection: Connection, sharedServices: LangiumSprotty return diagramSizes; }); } + +/** + * Adds handlers for requests regarding the fault tree creation. + * @param connection + */ +function addFTAGeneratorHandler(connection: Connection, sharedServices: LangiumSprottySharedServices): void { + // creates and serializes fault trees + connection.onRequest("generate/faultTrees", async (uri: string) => { + const models = await createFaultTrees(uri, sharedServices); + const texts = models.map((model) => serializeFTAAST(model)); + return texts; + }); +} diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 484ca26e..a1f31927 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -27,7 +27,7 @@ import { createSTPAResultMarkdownFile } from './report/md-export'; import { StpaResult } from './report/utils'; import { createSBMs } from './sbm/sbm-generation'; import { LTLFormula } from './sbm/utils'; -import { createOutputChannel, createQuickPickForWorkspaceOptions } from './utils'; +import { createFile, createOutputChannel, createQuickPickForWorkspaceOptions } from './utils'; let languageClient: LanguageClient; @@ -184,6 +184,17 @@ function registerSTPACommands(manager: StpaLspVscodeExtension, context: vscode.E }) ); + // command for creating fault trees + context.subscriptions.push( + vscode.commands.registerCommand(options.extensionPrefix + ".stpa.ft.generation", async (uri: vscode.Uri) => { + await manager.lsReady; + const texts: string[] = await languageClient.sendRequest("generate/faultTrees", uri.toString()); + const baseUri = uri.toString().substring(0, uri.toString().lastIndexOf("/")); + const fileName = uri.toString().substring(uri.toString().lastIndexOf("/"), uri.toString().lastIndexOf(".")); + texts.forEach((text, index) => createFile(`${baseUri}/generatedFTA${fileName}-fta${index}.fta`, text)); + }) + ); + // register commands that other extensions can use context.subscriptions.push( vscode.commands.registerCommand(command.getLTLFormula, async (uri: string) => { From ea55d3f15773cfd762e2938b87081858ef8ee5a5 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 24 Oct 2023 16:12:21 +0200 Subject: [PATCH 03/33] invisible node for gates with description (WIP) --- .../fta/diagram/fta-diagram-generator.ts | 169 +++++++++++++++--- .../fta/diagram/fta-interfaces.ts | 9 +- .../fta/diagram/fta-layout-config.ts | 44 ++++- .../fta/diagram/fta-model.ts | 13 ++ extension/src-language-server/fta/fta.langium | 8 +- extension/src-webview/di.config.ts | 6 +- extension/src-webview/fta/fta-model.ts | 18 ++ extension/src-webview/fta/fta-views.tsx | 18 +- 8 files changed, 245 insertions(+), 40 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index fc894f84..9f0fb2c5 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -17,22 +17,31 @@ import { AstNode } from "langium"; import { GeneratorContext, IdCache, LangiumDiagramGenerator } from "langium-sprotty"; -import { SLabel, SModelElement, SModelRoot } from "sprotty-protocol"; -import { ModelFTA, isComponent, isCondition, isKNGate } from "../../generated/ast"; +import { SLabel, SNode, SModelElement, SModelRoot } from "sprotty-protocol"; +import { Gate, ModelFTA, isComponent, isCondition, isKNGate } from "../../generated/ast"; import { FtaServices } from "../fta-module"; import { FtaSynthesisOptions, noCutSet, spofsSet } from "../fta-synthesis-options"; import { namedFtaElement } from "../utils"; -import { FTAEdge, FTANode } from "./fta-interfaces"; -import { FTA_EDGE_TYPE, FTA_GRAPH_TYPE, FTA_NODE_TYPE } from "./fta-model"; +import { FTAEdge, FTANode, FTAPort } from "./fta-interfaces"; +import { FTA_EDGE_TYPE, FTA_GRAPH_TYPE, FTA_INVISIBLE_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType, PortSide } from "./fta-model"; import { getFTNodeType, getTargets } from "./utils"; export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected readonly options: FtaSynthesisOptions; + + /** Saves the Ids of the generated SNodes */ + protected idToSNode: Map = new Map(); + + protected parentOfGate: Map = new Map(); + constructor(services: FtaServices) { super(services); this.options = services.options.SynthesisOptions; } + // TODO: replace with synthesis option + protected showDescriptions = true; + /** * Generates an SGraph for the FTA model contained in {@code args}. * @param args GeneratorContext for the FTA model. @@ -48,7 +57,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { this.generateFTNode(model.topEvent, idCache), ...model.components.map((component) => this.generateFTNode(component, idCache)), ...model.conditions.map((condition) => this.generateFTNode(condition, idCache)), - ...model.gates.map((gate) => this.generateFTNode(gate, idCache)), + ...model.gates.map((gate) => this.generateGate(gate, idCache)), // create edges for the gates and the top event ...model.gates.map((gate) => this.generateEdges(gate, idCache)).flat(1), ...this.generateEdges(model.topEvent, idCache), @@ -70,14 +79,34 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected generateEdges(node: AstNode, idCache: IdCache): SModelElement[] { const elements: SModelElement[] = []; const sourceId = idCache.getId(node); - // for every reference an edge is created - const targets = getTargets(node); - for (const target of targets) { - const targetId = idCache.getId(target); - const edgeId = idCache.uniqueId(`${sourceId}_${targetId}`, undefined); - if (sourceId && targetId) { - const e = this.generateFTEdge(edgeId, sourceId, targetId, idCache); - elements.push(e); + if (sourceId) { + // for every reference an edge is created + const targets = getTargets(node); + for (const target of targets) { + const targetId = idCache.getId(target); + const edgeId = idCache.uniqueId(`${sourceId}_${targetId}`, undefined); + + // create port for the source node + const sourceNode = this.idToSNode.get(sourceId); + const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); + sourceNode?.children?.push(this.createFTAPort(sourcePortId, PortSide.SOUTH)); + + // create port for parent and edge to this port + let parentPortId: string | undefined = undefined; + if (this.parentOfGate.has(sourceId)) { + const parent = this.parentOfGate.get(sourceId); + parentPortId = idCache.uniqueId(edgeId + "_newTransition"); + parent?.children?.push(this.createFTAPort(parentPortId, PortSide.SOUTH)); + const betweenEdgeId = idCache.uniqueId(edgeId + "_betweenEdge"); + const e = this.generateFTEdge(betweenEdgeId, sourcePortId, parentPortId, FTA_EDGE_TYPE, idCache); + elements.push(e); + } + + // create edge to target + if (sourceId && targetId) { + const e = this.generateFTEdge(edgeId, parentPortId ?? sourcePortId, targetId, FTA_EDGE_TYPE, idCache); + elements.push(e); + } } } return elements; @@ -96,12 +125,13 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { edgeId: string, sourceId: string, targetId: string, + type: string, idCache: IdCache, label?: string ): FTAEdge { const children = label ? this.createEdgeLabel(label, edgeId, idCache) : []; return { - type: FTA_EDGE_TYPE, + type: type, id: edgeId, sourceId: sourceId, targetId: targetId, @@ -110,6 +140,57 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { }; } + protected generateGate(node: Gate, idCache: IdCache): FTANode { + const gateNode = this.generateFTNode(node, idCache); + this.idToSNode.set(gateNode.id, gateNode); + if (!this.showDescriptions || node.description === undefined) { + return gateNode; + } + // create node for gate description + const descriptionNodeId = idCache.uniqueId(node.name + "Description"); + const descriptionNode = this.createNode( + descriptionNodeId, + node.description ?? "", + FTNodeType.DESCRIPTION, + "", + this.createNodeLabel(node.description, descriptionNodeId, idCache), + gateNode.inCurrentSelectedCutSet, + gateNode.notConnectedToSelectedCutSet + ); + + const invisibleEdge = this.generateFTEdge( + idCache.uniqueId(node.name + "InvisibleEdge"), + descriptionNode.id, + gateNode.id, + FTA_INVISIBLE_EDGE_TYPE, + idCache + ); + + // order is important to have the descriptionNode above the gateNode + const children: SModelElement[] = [descriptionNode, gateNode, invisibleEdge]; + this.idToSNode.set(descriptionNode.id, descriptionNode); + // create invisible node that contains the desciprion and gate node + const parent ={ + type: FTA_NODE_TYPE, + id: idCache.uniqueId(node.name + "Parent"), + name: node.name, + nodeType: FTNodeType.PARENT, + description: "", + children: children, + layout: "stack", + inCurrentSelectedCutSet: gateNode.inCurrentSelectedCutSet, + notConnectedToSelectedCutSet: gateNode.notConnectedToSelectedCutSet, + layoutOptions: { + paddingTop: 0.0, + paddingBottom: 10.0, + paddngLeft: 0.0, + paddingRight: 0.0, + }, + }; + this.parentOfGate.set(gateNode.id, parent); + return parent; + } + /** * Generates a single FTANode for the given {@code node}. * @param node The FTA component the node should be created for. @@ -128,13 +209,41 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { const spofs = this.options.getSpofs(); includedInCutSet = spofs.includes(node.name); notConnected = false; - } + } + + const ftNode = this.createNode( + nodeId, + node.name, + getFTNodeType(node), + description, + children, + includedInCutSet, + notConnected + ); + + this.idToSNode.set(nodeId, ftNode); - const ftNode = { + if (isKNGate(node)) { + ftNode.k = node.k; + ftNode.n = node.children.length; + } + return ftNode; + } + + protected createNode( + id: string, + name: string, + type: FTNodeType, + description: string, + children: SModelElement[], + includedInCutSet: boolean | undefined, + notConnected: boolean | undefined + ): FTANode { + return { type: FTA_NODE_TYPE, - id: nodeId, - name: node.name, - nodeType: getFTNodeType(node), + id: id, + name: name, + nodeType: type, description: description, children: children, layout: "stack", @@ -146,13 +255,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { paddngLeft: 10.0, paddingRight: 10.0, }, - } as FTANode; - - if (isKNGate(node)) { - ftNode.k = node.k; - ftNode.n = node.children.length; - } - return ftNode; + }; } /** @@ -188,4 +291,18 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { }, ]; } + + /** + * Creates an FTAPort. + * @param id The ID of the port. + * @param side The side of the port. + * @returns an FTAPort. + */ + protected createFTAPort(id: string, side: PortSide): FTAPort { + return { + type: FTA_PORT_TYPE, + id: id, + side: side, + }; + } } diff --git a/extension/src-language-server/fta/diagram/fta-interfaces.ts b/extension/src-language-server/fta/diagram/fta-interfaces.ts index 42f2390c..1ebf6f56 100644 --- a/extension/src-language-server/fta/diagram/fta-interfaces.ts +++ b/extension/src-language-server/fta/diagram/fta-interfaces.ts @@ -15,8 +15,8 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { SEdge, SNode } from "sprotty-protocol"; -import { FTNodeType } from "./fta-model"; +import { SEdge, SNode, SPort } from "sprotty-protocol"; +import { FTNodeType, PortSide } from "./fta-model"; /** * Node of a fault tree. @@ -37,3 +37,8 @@ export interface FTANode extends SNode { export interface FTAEdge extends SEdge { notConnectedToSelectedCutSet?: boolean; } + +/** Port representing a port in the FTA graph. */ +export interface FTAPort extends SPort { + side?: PortSide; +} diff --git a/extension/src-language-server/fta/diagram/fta-layout-config.ts b/extension/src-language-server/fta/diagram/fta-layout-config.ts index 004cb9a5..3bacbf51 100644 --- a/extension/src-language-server/fta/diagram/fta-layout-config.ts +++ b/extension/src-language-server/fta/diagram/fta-layout-config.ts @@ -17,7 +17,9 @@ import { LayoutOptions } from "elkjs"; import { DefaultLayoutConfigurator } from "sprotty-elk/lib/elk-layout"; -import { SGraph, SModelIndex, SNode } from "sprotty-protocol"; +import { SGraph, SModelIndex } from "sprotty-protocol"; +import { FTANode, FTAPort } from "./fta-interfaces"; +import { FTA_PORT_TYPE, FTNodeType, PortSide } from "./fta-model"; export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { protected graphOptions(_sgraph: SGraph, _index: SModelIndex): LayoutOptions { @@ -27,9 +29,41 @@ export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { }; } - protected nodeOptions(_snode: SNode, _index: SModelIndex): LayoutOptions | undefined { - return { - "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", - }; + protected nodeOptions(snode: FTANode, _index: SModelIndex): LayoutOptions | undefined { + switch (snode.nodeType) { + case FTNodeType.PARENT: + return { + "org.eclipse.elk.direction": "DOWN", + "org.eclipse.elk.padding": "[top=0.0,left=0.0,bottom=10.0,right=0.0]", + "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "0", + }; + default: + return { + "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", + }; + } + } + + protected portOptions(sport: FTAPort, index: SModelIndex): LayoutOptions | undefined { + if (sport.type === FTA_PORT_TYPE) { + let side = ''; + switch ((sport as FTAPort).side) { + case PortSide.WEST: + side = 'WEST'; + break; + case PortSide.EAST: + side = 'EAST'; + break; + case PortSide.NORTH: + side = 'NORTH'; + break; + case PortSide.SOUTH: + side = 'SOUTH'; + break; + } + return { + 'org.eclipse.elk.port.side': side + }; + } } } diff --git a/extension/src-language-server/fta/diagram/fta-model.ts b/extension/src-language-server/fta/diagram/fta-model.ts index 0829f10c..d5145aaa 100644 --- a/extension/src-language-server/fta/diagram/fta-model.ts +++ b/extension/src-language-server/fta/diagram/fta-model.ts @@ -18,7 +18,9 @@ /* fault tree element types */ export const FTA_NODE_TYPE = "node:fta"; export const FTA_EDGE_TYPE = "edge:fta"; +export const FTA_INVISIBLE_EDGE_TYPE = "edge:fta:invisible"; export const FTA_GRAPH_TYPE = "graph:fta"; +export const FTA_PORT_TYPE = 'port:fta'; /** * Types of fault tree nodes. @@ -31,5 +33,16 @@ export enum FTNodeType { OR, KN, INHIBIT, + DESCRIPTION, + PARENT, UNDEFINED, } + + +/** Possible sides for a port. */ +export enum PortSide { + WEST, + EAST, + NORTH, + SOUTH +} \ No newline at end of file diff --git a/extension/src-language-server/fta/fta.langium b/extension/src-language-server/fta/fta.langium index 02e7f0db..f85592e4 100644 --- a/extension/src-language-server/fta/fta.langium +++ b/extension/src-language-server/fta/fta.langium @@ -22,16 +22,16 @@ Children: Gate | Component; AND: - name=ID "=" children+=[Children:ID] ('and' children+=[Children:ID])+; + name=ID (description=STRING)? "=" children+=[Children:ID] ('and' children+=[Children:ID])+; OR: - name=ID "=" children+=[Children:ID] ('or' children+=[Children:ID])+; + name=ID (description=STRING)? "=" children+=[Children:ID] ('or' children+=[Children:ID])+; KNGate: - name=ID "=" k=INT 'of' children+=[Children:ID] (',' children+=[Children:ID])+; + name=ID (description=STRING)? "=" k=INT 'of' children+=[Children:ID] (',' children+=[Children:ID])+; InhibitGate: - name=ID "=" condition+=[Condition:ID] 'inhibits' children+=[Children:ID]; + name=ID (description=STRING)? "=" condition+=[Condition:ID] 'inhibits' children+=[Children:ID]; hidden terminal WS: /\s+/; terminal ID: /[_a-zA-Z][\w_]*/; diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index 1d49fee0..b92441e5 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -40,8 +40,8 @@ import { import { SvgCommand } from "./actions"; import { SvgPostprocessor } from "./exportPostProcessor"; import { CustomSvgExporter } from "./exporter"; -import { FTAEdge, FTANode, FTA_EDGE_TYPE, FTA_GRAPH_TYPE, FTA_NODE_TYPE } from "./fta/fta-model"; -import { FTAGraphView, FTANodeView, PolylineArrowEdgeViewFTA } from "./fta/fta-views"; +import { FTAEdge, FTANode, FTAPort, FTA_EDGE_TYPE, FTA_GRAPH_TYPE, FTA_INVISIBLE_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE } from "./fta/fta-model"; +import { FTAGraphView, FTAInvisibleEdgeView, FTANodeView, PolylineArrowEdgeViewFTA } from "./fta/fta-views"; import { PastaModelViewer } from "./model-viewer"; import { optionsModule } from "./options/options-module"; import { sidebarModule } from "./sidebar"; @@ -105,8 +105,10 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = // FTA configureModelElement(context, FTA_EDGE_TYPE, FTAEdge, PolylineArrowEdgeViewFTA); + configureModelElement(context, FTA_INVISIBLE_EDGE_TYPE, FTAEdge, FTAInvisibleEdgeView); configureModelElement(context, FTA_NODE_TYPE, FTANode, FTANodeView); configureModelElement(context, FTA_GRAPH_TYPE, SGraph, FTAGraphView); + configureModelElement(context, FTA_PORT_TYPE, FTAPort, PortView); }); export function createPastaDiagramContainer(widgetId: string): Container { diff --git a/extension/src-webview/fta/fta-model.ts b/extension/src-webview/fta/fta-model.ts index 5bb3ef29..06127cf8 100644 --- a/extension/src-webview/fta/fta-model.ts +++ b/extension/src-webview/fta/fta-model.ts @@ -18,6 +18,7 @@ import { SEdge, SNode, + SPort, connectableFeature, fadeFeature, hoverFeedbackFeature, @@ -29,7 +30,9 @@ import { /* fault tree element types */ export const FTA_NODE_TYPE = "node:fta"; export const FTA_EDGE_TYPE = "edge:fta"; +export const FTA_INVISIBLE_EDGE_TYPE = "edge:fta:invisible"; export const FTA_GRAPH_TYPE = "graph:fta"; +export const FTA_PORT_TYPE = "port:fta"; /** * Node of a fault tree. @@ -60,6 +63,11 @@ export class FTAEdge extends SEdge { notConnectedToSelectedCutSet?: boolean; } +/** Port representing a port in the FTA graph. */ +export class FTAPort extends SPort { + side?: PortSide; +} + /** * Types of fault tree nodes. */ @@ -71,5 +79,15 @@ export enum FTNodeType { OR, KN, INHIBIT, + DESCRIPTION, + PARENT, UNDEFINED, } + +/** Possible sides for a port. */ +export enum PortSide { + WEST, + EAST, + NORTH, + SOUTH, +} diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index 16215799..3d541b3d 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -18,7 +18,7 @@ /** @jsx svg */ import { injectable } from 'inversify'; import { VNode } from "snabbdom"; -import { Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SGraph, SGraphView, svg } from 'sprotty'; +import { IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, svg } from 'sprotty'; import { renderAndGate, renderOval, renderInhibitGate, renderKnGate, renderOrGate, renderRectangle } from "../views-rendering"; import { FTAEdge, FTANode, FTNodeType } from './fta-model'; @@ -38,6 +38,13 @@ export class PolylineArrowEdgeViewFTA extends PolylineEdgeView { } +@injectable() +export class FTAInvisibleEdgeView extends PolylineArrowEdgeViewFTA { + render(edge: Readonly, context: RenderingContext, args?: IViewArgs | undefined): VNode | undefined { + return ; + } +} + @injectable() export class FTANodeView extends RectangularNodeView { @@ -45,6 +52,14 @@ export class FTANodeView extends RectangularNodeView { // create the element based on the type of the node let element: VNode; switch (node.nodeType) { + case FTNodeType.PARENT: + /* return + {context.renderChildren(node)} + ; */ + case FTNodeType.DESCRIPTION: case FTNodeType.TOPEVENT: element = renderRectangle(node); break; @@ -94,6 +109,7 @@ export class FTAGraphView extends SGraphView { } protected highlightConnectedToCutSet(node: FTANode): void { + // TODO: must be adjusted for gate descriptions for (const edge of node.outgoingEdges) { (edge as FTAEdge).notConnectedToSelectedCutSet = true; const target = edge.target as FTANode; From 17cc26372570e52131d9b65e7f6d364a70b891cc Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 25 Oct 2023 09:24:43 +0200 Subject: [PATCH 04/33] invisible node for gates with description --- .../fta/diagram/fta-diagram-generator.ts | 88 ++++++++++++++----- .../fta/diagram/fta-layout-config.ts | 2 + extension/src-webview/fta/fta-views.tsx | 5 +- 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index 9f0fb2c5..652fd0c4 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -23,7 +23,15 @@ import { FtaServices } from "../fta-module"; import { FtaSynthesisOptions, noCutSet, spofsSet } from "../fta-synthesis-options"; import { namedFtaElement } from "../utils"; import { FTAEdge, FTANode, FTAPort } from "./fta-interfaces"; -import { FTA_EDGE_TYPE, FTA_GRAPH_TYPE, FTA_INVISIBLE_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType, PortSide } from "./fta-model"; +import { + FTA_EDGE_TYPE, + FTA_GRAPH_TYPE, + FTA_INVISIBLE_EDGE_TYPE, + FTA_NODE_TYPE, + FTA_PORT_TYPE, + FTNodeType, + PortSide, +} from "./fta-model"; import { getFTNodeType, getTargets } from "./utils"; export class FtaDiagramGenerator extends LangiumDiagramGenerator { @@ -33,6 +41,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected idToSNode: Map = new Map(); protected parentOfGate: Map = new Map(); + protected descriptionOfGate: Map = new Map(); constructor(services: FtaServices) { super(services); @@ -51,6 +60,10 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { const { document } = args; const model = document.parseResult.value; const idCache = args.idCache; + // reset maps + this.descriptionOfGate = new Map(); + this.parentOfGate = new Map(); + this.idToSNode = new Map(); const ftaChildren: SModelElement[] = [ // create nodes for top event, components, conditions, and gates @@ -85,26 +98,58 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { for (const target of targets) { const targetId = idCache.getId(target); const edgeId = idCache.uniqueId(`${sourceId}_${targetId}`, undefined); - + // create port for the source node const sourceNode = this.idToSNode.get(sourceId); const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); sourceNode?.children?.push(this.createFTAPort(sourcePortId, PortSide.SOUTH)); - // create port for parent and edge to this port - let parentPortId: string | undefined = undefined; + // create port for source parent and edge to this port + let sourceParentPortId: string | undefined = undefined; if (this.parentOfGate.has(sourceId)) { const parent = this.parentOfGate.get(sourceId); - parentPortId = idCache.uniqueId(edgeId + "_newTransition"); - parent?.children?.push(this.createFTAPort(parentPortId, PortSide.SOUTH)); + sourceParentPortId = idCache.uniqueId(edgeId + "_newTransition"); + parent?.children?.push(this.createFTAPort(sourceParentPortId, PortSide.SOUTH)); const betweenEdgeId = idCache.uniqueId(edgeId + "_betweenEdge"); - const e = this.generateFTEdge(betweenEdgeId, sourcePortId, parentPortId, FTA_EDGE_TYPE, idCache); - elements.push(e); + const e = this.generateFTEdge( + betweenEdgeId, + sourcePortId, + sourceParentPortId, + FTA_EDGE_TYPE, + idCache + ); + parent?.children?.push(e); } // create edge to target if (sourceId && targetId) { - const e = this.generateFTEdge(edgeId, parentPortId ?? sourcePortId, targetId, FTA_EDGE_TYPE, idCache); + let parentParentPortId: string | undefined = undefined; + // create port for target parent and edge to this port + if (this.parentOfGate.has(targetId)) { + const parent = this.parentOfGate.get(targetId); + parentParentPortId = idCache.uniqueId(edgeId + "_newTransition"); + parent?.children?.push(this.createFTAPort(parentParentPortId, PortSide.NORTH)); + const betweenEdgeId = idCache.uniqueId(edgeId + "_betweenEdge"); + const descriptionId = this.descriptionOfGate.get(targetId)?.id; + if (descriptionId) { + const invisibleEdge = this.generateFTEdge( + betweenEdgeId, + parentParentPortId, + descriptionId, + FTA_INVISIBLE_EDGE_TYPE, + idCache + ); + elements.push(invisibleEdge); + } + } + + const e = this.generateFTEdge( + edgeId, + sourceParentPortId ?? sourcePortId, + parentParentPortId ?? targetId, + FTA_EDGE_TYPE, + idCache + ); elements.push(e); } } @@ -169,8 +214,9 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { // order is important to have the descriptionNode above the gateNode const children: SModelElement[] = [descriptionNode, gateNode, invisibleEdge]; this.idToSNode.set(descriptionNode.id, descriptionNode); + this.descriptionOfGate.set(gateNode.id, descriptionNode); // create invisible node that contains the desciprion and gate node - const parent ={ + const parent = { type: FTA_NODE_TYPE, id: idCache.uniqueId(node.name + "Parent"), name: node.name, @@ -198,7 +244,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { * @returns a FTANode representing {@code node}. */ protected generateFTNode(node: namedFtaElement, idCache: IdCache): FTANode { - const nodeId = idCache.uniqueId(node.name, node); + const nodeId = idCache.uniqueId(node.name.replace(" ", ""), node); const children: SModelElement[] = this.createNodeLabel(node.name, nodeId, idCache); const description = isComponent(node) || isCondition(node) ? node.description : ""; const set = this.options.getCutSet(); @@ -220,7 +266,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { includedInCutSet, notConnected ); - + this.idToSNode.set(nodeId, ftNode); if (isKNGate(node)) { @@ -269,7 +315,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { return [ { type: "label", - id: idCache.uniqueId(id + ".label"), + id: idCache.uniqueId(id + "_label"), text: label, }, ]; @@ -286,7 +332,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { return [ { type: "label:xref", - id: idCache.uniqueId(id + ".label"), + id: idCache.uniqueId(id + "_label"), text: label, }, ]; @@ -298,11 +344,11 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { * @param side The side of the port. * @returns an FTAPort. */ - protected createFTAPort(id: string, side: PortSide): FTAPort { - return { - type: FTA_PORT_TYPE, - id: id, - side: side, - }; - } + protected createFTAPort(id: string, side: PortSide): FTAPort { + return { + type: FTA_PORT_TYPE, + id: id, + side: side, + }; + } } diff --git a/extension/src-language-server/fta/diagram/fta-layout-config.ts b/extension/src-language-server/fta/diagram/fta-layout-config.ts index 3bacbf51..ef846ecd 100644 --- a/extension/src-language-server/fta/diagram/fta-layout-config.ts +++ b/extension/src-language-server/fta/diagram/fta-layout-config.ts @@ -26,6 +26,7 @@ export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { return { "org.eclipse.elk.direction": "DOWN", "org.eclipse.elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", + 'org.eclipse.elk.portConstraints': 'FIXED_SIDE' }; } @@ -36,6 +37,7 @@ export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { "org.eclipse.elk.direction": "DOWN", "org.eclipse.elk.padding": "[top=0.0,left=0.0,bottom=10.0,right=0.0]", "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "0", + 'org.eclipse.elk.portConstraints': 'FIXED_SIDE' }; default: return { diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index 3d541b3d..669b9146 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -53,12 +53,13 @@ export class FTANodeView extends RectangularNodeView { let element: VNode; switch (node.nodeType) { case FTNodeType.PARENT: - /* return {context.renderChildren(node)} - ; */ + ; case FTNodeType.DESCRIPTION: case FTNodeType.TOPEVENT: element = renderRectangle(node); From f587abae35571187b0b5e0af74cebb47580cd88a Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 25 Oct 2023 11:01:09 +0200 Subject: [PATCH 05/33] adjust cut set highlighting for gates with description --- .../fta/diagram/fta-diagram-generator.ts | 20 +++++---- extension/src-webview/fta/fta-views.tsx | 41 +++++++++++++------ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index 652fd0c4..c98bc464 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -122,19 +122,24 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { } // create edge to target - if (sourceId && targetId) { - let parentParentPortId: string | undefined = undefined; - // create port for target parent and edge to this port + if (targetId) { + // create port for the target node + const targetNode = this.idToSNode.get(targetId); + const targetPortId = idCache.uniqueId(edgeId + "_newTransition"); + targetNode?.children?.push(this.createFTAPort(targetPortId, PortSide.NORTH)); + + let targetParentPortId: string | undefined = undefined; + // create port for target parent and edge from this port to description node if (this.parentOfGate.has(targetId)) { const parent = this.parentOfGate.get(targetId); - parentParentPortId = idCache.uniqueId(edgeId + "_newTransition"); - parent?.children?.push(this.createFTAPort(parentParentPortId, PortSide.NORTH)); + targetParentPortId = idCache.uniqueId(edgeId + "_newTransition"); + parent?.children?.push(this.createFTAPort(targetParentPortId, PortSide.NORTH)); const betweenEdgeId = idCache.uniqueId(edgeId + "_betweenEdge"); const descriptionId = this.descriptionOfGate.get(targetId)?.id; if (descriptionId) { const invisibleEdge = this.generateFTEdge( betweenEdgeId, - parentParentPortId, + targetParentPortId, descriptionId, FTA_INVISIBLE_EDGE_TYPE, idCache @@ -143,10 +148,11 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { } } + // create edge from source to target const e = this.generateFTEdge( edgeId, sourceParentPortId ?? sourcePortId, - parentParentPortId ?? targetId, + targetParentPortId ?? targetPortId, FTA_EDGE_TYPE, idCache ); diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index 669b9146..6e27f3df 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -20,7 +20,7 @@ import { injectable } from 'inversify'; import { VNode } from "snabbdom"; import { IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, svg } from 'sprotty'; import { renderAndGate, renderOval, renderInhibitGate, renderKnGate, renderOrGate, renderRectangle } from "../views-rendering"; -import { FTAEdge, FTANode, FTNodeType } from './fta-model'; +import { FTAEdge, FTANode, FTAPort, FTA_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType } from './fta-model'; @injectable() export class PolylineArrowEdgeViewFTA extends PolylineEdgeView { @@ -57,7 +57,7 @@ export class FTANodeView extends RectangularNodeView { return + class-greyed-out={false}> {context.renderChildren(node)} ; case FTNodeType.DESCRIPTION: @@ -103,23 +103,40 @@ export class FTAGraphView extends SGraphView { render(model: Readonly, context: RenderingContext): VNode { if (model.children.length !== 0) { - this.highlightConnectedToCutSet(model.children[0] as FTANode); + this.highlightConnectedToCutSet(model, model.children[0] as FTANode); } return super.render(model, context); } - protected highlightConnectedToCutSet(node: FTANode): void { - // TODO: must be adjusted for gate descriptions - for (const edge of node.outgoingEdges) { - (edge as FTAEdge).notConnectedToSelectedCutSet = true; - const target = edge.target as FTANode; - this.highlightConnectedToCutSet(target); - if (!target.notConnectedToSelectedCutSet) { - node.notConnectedToSelectedCutSet = false; - (edge as FTAEdge).notConnectedToSelectedCutSet = false; + protected highlightConnectedToCutSet(model: SGraph, currentNode: FTANode): void { + for (const port of currentNode.children.filter(child => child.type === FTA_PORT_TYPE)) { + const edge = model.children.find(child => child.type === FTA_EDGE_TYPE && (child as FTAEdge).sourceId === port.id) as FTAEdge; + if (edge) { + edge.notConnectedToSelectedCutSet = true; + const target = (edge.target as FTAPort).parent as FTANode; + // handle successor nodes + this.highlightConnectedToCutSet(model, target); + // handle current node + if (!target.notConnectedToSelectedCutSet) { + currentNode.notConnectedToSelectedCutSet = false; + edge.notConnectedToSelectedCutSet = false; + } + // handle edges in parents + if (currentNode.nodeType === FTNodeType.PARENT) { + const innerEdge = currentNode.children.find(child => child.type === FTA_EDGE_TYPE && (child as FTAEdge).targetId === edge.sourceId) as FTAEdge; + innerEdge.notConnectedToSelectedCutSet = edge.notConnectedToSelectedCutSet; + } } } + // handle nodes in parents + if (currentNode.nodeType === FTNodeType.PARENT) { + currentNode.children.forEach(child => { + if (child.type === FTA_NODE_TYPE) { + (child as FTANode).notConnectedToSelectedCutSet = currentNode.notConnectedToSelectedCutSet; + } + }); + } } } \ No newline at end of file From 0b308c130965a5c3c48ef0e9d453db367cf2cbff Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 26 Oct 2023 12:22:00 +0200 Subject: [PATCH 06/33] stpa to fta --- .../fta/diagram/fta-diagram-generator.ts | 11 +- .../fta/fta-message-handler.ts | 6 +- extension/src-language-server/fta/fta.langium | 8 +- extension/src-language-server/fta/utils.ts | 30 +++-- .../stpa/diagram/filtering.ts | 4 +- .../stpa/ftaGeneration/fta-generation.ts | 103 +++++++++++++++--- 6 files changed, 126 insertions(+), 36 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index c98bc464..926dfe56 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -17,7 +17,7 @@ import { AstNode } from "langium"; import { GeneratorContext, IdCache, LangiumDiagramGenerator } from "langium-sprotty"; -import { SLabel, SNode, SModelElement, SModelRoot } from "sprotty-protocol"; +import { SLabel, SModelElement, SModelRoot, SNode } from "sprotty-protocol"; import { Gate, ModelFTA, isComponent, isCondition, isKNGate } from "../../generated/ast"; import { FtaServices } from "../fta-module"; import { FtaSynthesisOptions, noCutSet, spofsSet } from "../fta-synthesis-options"; @@ -67,15 +67,20 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { const ftaChildren: SModelElement[] = [ // create nodes for top event, components, conditions, and gates - this.generateFTNode(model.topEvent, idCache), ...model.components.map((component) => this.generateFTNode(component, idCache)), ...model.conditions.map((condition) => this.generateFTNode(condition, idCache)), ...model.gates.map((gate) => this.generateGate(gate, idCache)), // create edges for the gates and the top event ...model.gates.map((gate) => this.generateEdges(gate, idCache)).flat(1), - ...this.generateEdges(model.topEvent, idCache), ]; + if (model.topEvent) { + ftaChildren.push( + this.generateFTNode(model.topEvent, idCache), + ...this.generateEdges(model.topEvent, idCache) + ); + } + return { type: FTA_GRAPH_TYPE, id: "root", diff --git a/extension/src-language-server/fta/fta-message-handler.ts b/extension/src-language-server/fta/fta-message-handler.ts index 0a2c6791..a3d1bea0 100644 --- a/extension/src-language-server/fta/fta-message-handler.ts +++ b/extension/src-language-server/fta/fta-message-handler.ts @@ -22,6 +22,7 @@ import { determineMinimalCutSets, determineCutSetsForFT } from "./analysis/fta-c import { FtaServices } from "./fta-module"; import { cutSetsToString } from "./utils"; import { ModelFTA } from "../generated/ast"; +import { AstNode } from "langium"; /** * Adds handlers for notifications regarding fta. @@ -71,7 +72,10 @@ async function cutSetsRequested( minimal: boolean ): Promise { const model = (await getModel(uri, sharedServices)) as ModelFTA; - const nodes = [model.topEvent, ...model.components, ...model.conditions, ...model.gates]; + const nodes: AstNode[] = [...model.components, ...model.conditions, ...model.gates]; + if (model.topEvent) { + nodes.push(model.topEvent); + } const cutSets = minimal ? determineMinimalCutSets(nodes) : determineCutSetsForFT(nodes); // determine single points of failure const spofs: string[] = []; diff --git a/extension/src-language-server/fta/fta.langium b/extension/src-language-server/fta/fta.langium index f85592e4..50a885f3 100644 --- a/extension/src-language-server/fta/fta.langium +++ b/extension/src-language-server/fta/fta.langium @@ -3,7 +3,7 @@ grammar Fta entry ModelFTA: ('Components' components+=Component*)? ('Conditions' conditions+=Condition*)? - ('TopEvent' topEvent=TopEvent) + ('TopEvent' topEvent=TopEvent)? ('Gates' gates+=Gate*)?; Component: @@ -22,13 +22,13 @@ Children: Gate | Component; AND: - name=ID (description=STRING)? "=" children+=[Children:ID] ('and' children+=[Children:ID])+; + name=ID (description=STRING)? "=" children+=[Children:ID] ('and' children+=[Children:ID])*; OR: - name=ID (description=STRING)? "=" children+=[Children:ID] ('or' children+=[Children:ID])+; + name=ID (description=STRING)? "=" children+=[Children:ID] ('or' children+=[Children:ID])*; KNGate: - name=ID (description=STRING)? "=" k=INT 'of' children+=[Children:ID] (',' children+=[Children:ID])+; + name=ID (description=STRING)? "=" k=INT 'of' children+=[Children:ID] (',' children+=[Children:ID])*; InhibitGate: name=ID (description=STRING)? "=" condition+=[Condition:ID] 'inhibits' children+=[Children:ID]; diff --git a/extension/src-language-server/fta/utils.ts b/extension/src-language-server/fta/utils.ts index 39ad886c..7e6081b2 100644 --- a/extension/src-language-server/fta/utils.ts +++ b/extension/src-language-server/fta/utils.ts @@ -43,13 +43,14 @@ export function cutSetsToString(cutSets: Set[]): string[] { */ export function getRangeOfNodeFTA(model: ModelFTA, label: string): Range | undefined { let range: Range | undefined = undefined; - const elements: namedFtaElement[] = [model.topEvent, ...model.components, ...model.conditions, ...model.gates]; - elements.forEach((component) => { - if (component.name === label) { - range = component.$cstNode?.range; - return; - } - }); + const elements: namedFtaElement[] = [...model.components, ...model.conditions, ...model.gates]; + if (model.topEvent) { + elements.push(model.topEvent); + } + const selectedElement = elements.find((element) => element.name === label); + if (selectedElement) { + range = selectedElement.$cstNode?.range; + } return range; } @@ -63,26 +64,33 @@ export function serializeFTAAST(model: ModelFTA): string { if (model.components && model.components.length !== 0) { result += "Components\n"; model.components.forEach((component) => (result += `${component.name} "${component.description}"\n`)); + result += "\n"; } if (model.conditions && model.conditions.length !== 0) { result += "Conditions\n"; model.conditions.forEach((condition) => (result += `${condition.name} "${condition.description}"\n`)); + result += "\n"; } if (model.topEvent) { result += "TopEvent\n"; result += `"${model.topEvent.name}" = ${model.topEvent.child.$refText}\n`; + result += "\n"; } if (model.gates && model.gates.length !== 0) { result += "Gates\n"; model.gates.forEach((gate) => { + result += `${gate.name}`; + if (gate.description) { + result += ` "${gate.description}"`; + } if (isAND(gate)) { - result += `${gate.name} = ${gate.children.join(" and ")}\n`; + result += ` = ${gate.children.map(child => child.$refText).join(" and ")}\n`; } else if (isOR(gate)) { - result += `${gate.name} = ${gate.children.join(" or ")}\n`; + result += ` = ${gate.children.map(child => child.$refText).join(" or ")}\n`; } else if (isKNGate(gate)) { - result += `${gate.name} = ${gate.k} of ${gate.children.join(", ")}\n`; + result += ` = ${gate.k} of ${gate.children.map(child => child.$refText).join(", ")}\n`; } else if (isInhibitGate(gate)) { - result += `${gate.name} = ${gate.condition} inhibits ${gate.children.join("")}\n`; + result += ` = ${gate.condition} inhibits ${gate.children.map(child => child.$refText).join("")}\n`; } }); } diff --git a/extension/src-language-server/stpa/diagram/filtering.ts b/extension/src-language-server/stpa/diagram/filtering.ts index 16a52837..2db10f1a 100644 --- a/extension/src-language-server/stpa/diagram/filtering.ts +++ b/extension/src-language-server/stpa/diagram/filtering.ts @@ -135,12 +135,12 @@ function setFilterUCAOption(allUCAs: ActionUCAs[], rules: Rule[], options: StpaS const set = new Set(); set.add("all UCAs"); // collect all available control actions - allUCAs.forEach((uca) => { + allUCAs?.forEach((uca) => { if (!set.has(uca.system.ref?.name + "." + uca.action.ref?.name)) { set.add(uca.system.ref?.name + "." + uca.action.ref?.name); } }); - rules.forEach((rule) => { + rules?.forEach((rule) => { if (!set.has(rule.system.ref?.name + "." + rule.action.ref?.name)) { set.add(rule.system.ref?.name + "." + rule.action.ref?.name); } diff --git a/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts b/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts index 00a43de3..abb46ebc 100644 --- a/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts +++ b/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts @@ -15,8 +15,9 @@ * SPDX-License-Identifier: EPL-2.0 */ +import type { Reference } from "langium"; import { LangiumSprottySharedServices } from "langium-sprotty"; -import { Component, Hazard, Model, ModelFTA } from "../../generated/ast"; +import { Children, Component, Hazard, LossScenario, Model, ModelFTA, OR, TopEvent, isOR } from "../../generated/ast"; import { getModel } from "../../utils"; /** @@ -29,38 +30,110 @@ export async function createFaultTrees(uri: string, shared: LangiumSprottyShared // get the current model const model = (await getModel(uri, shared)) as Model; const faultTrees: ModelFTA[] = []; + // sort scenarios based on their hazard + const scenarios: Map = sortScenarios(model); + // create fault tree for each hazard for (const hazard of model.hazards) { - faultTrees.push(createFaulTreeForHazard(model, hazard)); + faultTrees.push(createFaulTreeForHazard(model, scenarios, hazard)); } return faultTrees; } +/** + * Sorts the loss scenarios in {@code model} based on their hazard. + * @param model The model containing the loss scenarios. + * @returns the sorted loss scenarios. + */ +function sortScenarios(model: Model): Map { + const scenarios: Map = new Map(); + for (const scenario of model.scenarios) { + const hazards = scenario.list?.refs; + for (const hazard of hazards || []) { + const hazardName = hazard.$refText; + addToListMap(scenarios, hazardName, scenario); + } + } + return scenarios; +} + /** * Creates a fault tree with {@code hazard} as top event. * @param stpaModel The stpa model that contains the {@code hazard}. * @param hazard The hazard for which the fault tree should be created. * @returns the AST of the created fault tree with {@code hazard} as top event. */ -function createFaulTreeForHazard(stpaModel: Model, hazard: Hazard): ModelFTA { +function createFaulTreeForHazard(stpaModel: Model, scenarios: Map, hazard: Hazard): ModelFTA { const ftaModel = {} as ModelFTA; ftaModel.components = []; ftaModel.gates = []; - const component = { name: hazard.name, description: hazard.description } as Component; - ftaModel.components.push(component); + // add scenarios as components and sort them by their causal factor + const causalFactors: Map = new Map(); + for (const scenario of scenarios.get(hazard.name) || []) { + const component = { + name: scenario.name, + description: scenario.description, + $container: ftaModel, + $type: "Component", + } as Component; + ftaModel.components.push(component); + const causalFactor = scenario.factor; + if (causalFactor) { + addToListMap(causalFactors, causalFactor, scenario); + } + } - const scenarios = stpaModel.scenarios.filter((scenario) => { - if (scenario.list?.refs?.find((ref) => ref.$refText === hazard.name) !== undefined) { - return true; - } else { - return false; + // create gate for each causal factor + let counter = 1; + for (const causalFactor of causalFactors.keys()) { + const children = causalFactors.get(causalFactor)?.map((scenario) => { + return { + ref: ftaModel.components.find((component) => component.name === scenario.name), + $refText: scenario.name, + } as Reference; + }); + if (children) { + const gate = { + name: `G${counter}`, + description: causalFactor, + children, + $container: ftaModel, + $type: "OR", + } as OR; + ftaModel.gates.push(gate); + counter++; } - }); - for (const scenario of scenarios) { - const component = { name: scenario.name, description: scenario.description } as Component; - ftaModel.components.push(component); } - //TODO: implement + + // create gate to connect top event with all other gates + const gateChildren = ftaModel.gates.map((gate) => { return { ref: gate, $refText: gate.name } as Reference; }); + const gate = { + name: `G0`, + children: gateChildren, + $container: ftaModel, + $type: "OR", + } as OR; + ftaModel.gates.push(gate); + + // create top event + const topEvent = { + name: hazard.description, + child: { ref: gate, $refText: gate.name }, + $container: ftaModel, + $type: "TopEvent", + } as TopEvent; + ftaModel.topEvent = topEvent; return ftaModel; } + +function addToListMap(map: Map, key: string, value: any): void { + if (map.has(key)) { + const currentValues = map.get(key); + if (currentValues) { + currentValues.push(value); + } + } else { + map.set(key, [value]); + } +} From 47daec81d96f6cd12cc48295a090c8b62e80aa33 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 26 Oct 2023 14:48:28 +0200 Subject: [PATCH 07/33] resetting cut set option for new model --- .../src-language-server/fta/fta-message-handler.ts | 13 +++++++++++-- .../fta/fta-synthesis-options.ts | 12 ++++++++++++ extension/src/extension.ts | 5 +++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/extension/src-language-server/fta/fta-message-handler.ts b/extension/src-language-server/fta/fta-message-handler.ts index a3d1bea0..933a0716 100644 --- a/extension/src-language-server/fta/fta-message-handler.ts +++ b/extension/src-language-server/fta/fta-message-handler.ts @@ -23,6 +23,7 @@ import { FtaServices } from "./fta-module"; import { cutSetsToString } from "./utils"; import { ModelFTA } from "../generated/ast"; import { AstNode } from "langium"; +import { noCutSet } from "./fta-synthesis-options"; /** * Adds handlers for notifications regarding fta. @@ -49,12 +50,15 @@ function addCutSetsHandler( ftaServices: FtaServices, sharedServices: LangiumSprottySharedServices ): void { - connection.onRequest("generate/getCutSets", async (uri: string) => { + connection.onRequest("cutSets/generate", async (uri: string) => { return cutSetsRequested(uri, ftaServices, sharedServices, false); }); - connection.onRequest("generate/getMinimalCutSets", async (uri: string) => { + connection.onRequest("cutSets/generateMinimal", async (uri: string) => { return cutSetsRequested(uri, ftaServices, sharedServices, true); }); + connection.onRequest("cutSets/reset", () => { + return resetCutSets(ftaServices); + }); } /** @@ -93,3 +97,8 @@ async function cutSetsRequested( ftaServices.options.SynthesisOptions.updateCutSetsOption(dropdownValues); return cutSetsString; } + +function resetCutSets(ftaServices: FtaServices): void { + ftaServices.options.SynthesisOptions.resetCutSets(); + return; +} \ No newline at end of file diff --git a/extension/src-language-server/fta/fta-synthesis-options.ts b/extension/src-language-server/fta/fta-synthesis-options.ts index df433e10..27d27d1c 100644 --- a/extension/src-language-server/fta/fta-synthesis-options.ts +++ b/extension/src-language-server/fta/fta-synthesis-options.ts @@ -64,6 +64,18 @@ export class FtaSynthesisOptions extends SynthesisOptions { } } + resetCutSets(): void { + const option = this.getOption(cutSetsID); + if (option) { + (option.synthesisOption as DropDownOption).availableValues = [noCutSet]; + (option.synthesisOption as DropDownOption).values = [noCutSet]; + (option.synthesisOption as DropDownOption).currentId = noCutSet.id; + option.synthesisOption.currentValue = noCutSet.id; + option.synthesisOption.initialValue = noCutSet.id; + option.currentValue = noCutSet.id; + } + } + setCutSet(value: string): void { const option = this.options.find((option) => option.synthesisOption.id === cutSetsID); if (option) { diff --git a/extension/src/extension.ts b/extension/src/extension.ts index a1f31927..843da6f8 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -213,14 +213,14 @@ function registerFTACommands(manager: StpaLspVscodeExtension, context: vscode.Ex // commands for computing and displaying the (minimal) cut sets of the fault tree. context.subscriptions.push( vscode.commands.registerCommand(options.extensionPrefix + ".fta.cutSets", async (uri: vscode.Uri) => { - const cutSets: string[] = await languageClient.sendRequest("generate/getCutSets", uri.path); + const cutSets: string[] = await languageClient.sendRequest("cutSets/generate", uri.path); await manager.openDiagram(uri); handleCutSets(manager, cutSets, false); }) ); context.subscriptions.push( vscode.commands.registerCommand(options.extensionPrefix + ".fta.minimalCutSets", async (uri: vscode.Uri) => { - const minimalCutSets: string[] = await languageClient.sendRequest("generate/getMinimalCutSets", uri.path); + const minimalCutSets: string[] = await languageClient.sendRequest("cutSets/generateMinimal", uri.path); await manager.openDiagram(uri); handleCutSets(manager, minimalCutSets, true); }) @@ -285,6 +285,7 @@ function registerTextEditorSync(manager: StpaLspVscodeExtension, context: vscode context.subscriptions.push( vscode.workspace.onDidSaveTextDocument(async document => { if (document) { + await languageClient.sendRequest('cutSets/reset'); manager.openDiagram(document.uri); if (manager.contextTable) { languageClient.sendNotification('contextTable/getData', document.uri.toString()); From daabd9e70a217f450b2ad86cefc07734a6bec3fe Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 26 Oct 2023 14:48:43 +0200 Subject: [PATCH 08/33] adjusted styling of description node (WIP) --- .../fta/diagram/fta-diagram-generator.ts | 75 +++++++++++-------- .../fta/diagram/fta-interfaces.ts | 6 ++ .../fta/diagram/fta-model.ts | 2 +- extension/src-webview/css/fta-diagram.css | 10 +++ extension/src-webview/di.config.ts | 5 +- extension/src-webview/fta/fta-model.ts | 17 ++++- extension/src-webview/fta/fta-views.tsx | 34 +++++++-- extension/src-webview/views-rendering.tsx | 30 +++++++- 8 files changed, 135 insertions(+), 44 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index 926dfe56..549391df 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -22,8 +22,9 @@ import { Gate, ModelFTA, isComponent, isCondition, isKNGate } from "../../genera import { FtaServices } from "../fta-module"; import { FtaSynthesisOptions, noCutSet, spofsSet } from "../fta-synthesis-options"; import { namedFtaElement } from "../utils"; -import { FTAEdge, FTANode, FTAPort } from "./fta-interfaces"; +import { DescriptionNode, FTAEdge, FTANode, FTAPort } from "./fta-interfaces"; import { + FTA_DESCRIPTION_NODE_TYPE, FTA_EDGE_TYPE, FTA_GRAPH_TYPE, FTA_INVISIBLE_EDGE_TYPE, @@ -42,6 +43,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected parentOfGate: Map = new Map(); protected descriptionOfGate: Map = new Map(); + protected parentToPort: Map = new Map(); constructor(services: FtaServices) { super(services); @@ -106,14 +108,14 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { // create port for the source node const sourceNode = this.idToSNode.get(sourceId); - const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); + const sourcePortId = idCache.uniqueId(edgeId + "_port"); sourceNode?.children?.push(this.createFTAPort(sourcePortId, PortSide.SOUTH)); // create port for source parent and edge to this port let sourceParentPortId: string | undefined = undefined; if (this.parentOfGate.has(sourceId)) { const parent = this.parentOfGate.get(sourceId); - sourceParentPortId = idCache.uniqueId(edgeId + "_newTransition"); + sourceParentPortId = idCache.uniqueId(edgeId + "_port"); parent?.children?.push(this.createFTAPort(sourceParentPortId, PortSide.SOUTH)); const betweenEdgeId = idCache.uniqueId(edgeId + "_betweenEdge"); const e = this.generateFTEdge( @@ -130,26 +132,27 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { if (targetId) { // create port for the target node const targetNode = this.idToSNode.get(targetId); - const targetPortId = idCache.uniqueId(edgeId + "_newTransition"); + const targetPortId = idCache.uniqueId(edgeId + "_port"); targetNode?.children?.push(this.createFTAPort(targetPortId, PortSide.NORTH)); let targetParentPortId: string | undefined = undefined; - // create port for target parent and edge from this port to description node + // create edge from parent port to description node if (this.parentOfGate.has(targetId)) { const parent = this.parentOfGate.get(targetId); - targetParentPortId = idCache.uniqueId(edgeId + "_newTransition"); - parent?.children?.push(this.createFTAPort(targetParentPortId, PortSide.NORTH)); - const betweenEdgeId = idCache.uniqueId(edgeId + "_betweenEdge"); - const descriptionId = this.descriptionOfGate.get(targetId)?.id; - if (descriptionId) { - const invisibleEdge = this.generateFTEdge( - betweenEdgeId, - targetParentPortId, - descriptionId, - FTA_INVISIBLE_EDGE_TYPE, - idCache - ); - elements.push(invisibleEdge); + targetParentPortId = this.parentToPort.get(parent?.id ?? "")?.id; + if (targetParentPortId) { + const betweenEdgeId = idCache.uniqueId(edgeId + "_betweenEdge"); + const descriptionId = this.descriptionOfGate.get(targetId)?.id; + if (descriptionId) { + const invisibleEdge = this.generateFTEdge( + betweenEdgeId, + targetParentPortId, + descriptionId, + FTA_INVISIBLE_EDGE_TYPE, + idCache + ); + elements.push(invisibleEdge); + } } } @@ -204,15 +207,21 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { } // create node for gate description const descriptionNodeId = idCache.uniqueId(node.name + "Description"); - const descriptionNode = this.createNode( - descriptionNodeId, - node.description ?? "", - FTNodeType.DESCRIPTION, - "", - this.createNodeLabel(node.description, descriptionNodeId, idCache), - gateNode.inCurrentSelectedCutSet, - gateNode.notConnectedToSelectedCutSet - ); + const descriptionNode: DescriptionNode = { + type: FTA_DESCRIPTION_NODE_TYPE, + id: descriptionNodeId, + name: node.description ?? "", + children: this.createNodeLabel(node.description, descriptionNodeId, idCache), + layout: "stack", + inCurrentSelectedCutSet: gateNode.inCurrentSelectedCutSet, + notConnectedToSelectedCutSet: gateNode.notConnectedToSelectedCutSet, + layoutOptions: { + paddingTop: 10.0, + paddingBottom: 10.0, + paddngLeft: 10.0, + paddingRight: 10.0, + }, + }; const invisibleEdge = this.generateFTEdge( idCache.uniqueId(node.name + "InvisibleEdge"), @@ -223,13 +232,13 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { ); // order is important to have the descriptionNode above the gateNode - const children: SModelElement[] = [descriptionNode, gateNode, invisibleEdge]; - this.idToSNode.set(descriptionNode.id, descriptionNode); - this.descriptionOfGate.set(gateNode.id, descriptionNode); + const parentId = idCache.uniqueId(node.name + "Parent"); + const port = this.createFTAPort(idCache.uniqueId(parentId + "_port"), PortSide.NORTH); + const children: SModelElement[] = [descriptionNode, gateNode, invisibleEdge, port]; // create invisible node that contains the desciprion and gate node const parent = { type: FTA_NODE_TYPE, - id: idCache.uniqueId(node.name + "Parent"), + id: parentId, name: node.name, nodeType: FTNodeType.PARENT, description: "", @@ -244,7 +253,11 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { paddingRight: 0.0, }, }; + + this.idToSNode.set(descriptionNode.id, descriptionNode); + this.descriptionOfGate.set(gateNode.id, descriptionNode); this.parentOfGate.set(gateNode.id, parent); + this.parentToPort.set(parent.id, port); return parent; } diff --git a/extension/src-language-server/fta/diagram/fta-interfaces.ts b/extension/src-language-server/fta/diagram/fta-interfaces.ts index 1ebf6f56..7cb9aceb 100644 --- a/extension/src-language-server/fta/diagram/fta-interfaces.ts +++ b/extension/src-language-server/fta/diagram/fta-interfaces.ts @@ -31,6 +31,12 @@ export interface FTANode extends SNode { n?: number; } +export interface DescriptionNode extends SNode { + name: string; + inCurrentSelectedCutSet?: boolean; + notConnectedToSelectedCutSet?: boolean; +} + /** * Edge of a fault tree. */ diff --git a/extension/src-language-server/fta/diagram/fta-model.ts b/extension/src-language-server/fta/diagram/fta-model.ts index d5145aaa..ada3788a 100644 --- a/extension/src-language-server/fta/diagram/fta-model.ts +++ b/extension/src-language-server/fta/diagram/fta-model.ts @@ -17,6 +17,7 @@ /* fault tree element types */ export const FTA_NODE_TYPE = "node:fta"; +export const FTA_DESCRIPTION_NODE_TYPE = "node:fta:description"; export const FTA_EDGE_TYPE = "edge:fta"; export const FTA_INVISIBLE_EDGE_TYPE = "edge:fta:invisible"; export const FTA_GRAPH_TYPE = "graph:fta"; @@ -33,7 +34,6 @@ export enum FTNodeType { OR, KN, INHIBIT, - DESCRIPTION, PARENT, UNDEFINED, } diff --git a/extension/src-webview/css/fta-diagram.css b/extension/src-webview/css/fta-diagram.css index edb0f761..1a349cff 100644 --- a/extension/src-webview/css/fta-diagram.css +++ b/extension/src-webview/css/fta-diagram.css @@ -46,3 +46,13 @@ .fta-highlight-node{ stroke: var(--highlight-node); } + +.gate-description { + stroke: none; + fill: darkgrey; + fill-opacity: 20%; +} + +.vertical-edge { + stroke: darkgray; +} \ No newline at end of file diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index b92441e5..538f61c3 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -40,8 +40,8 @@ import { import { SvgCommand } from "./actions"; import { SvgPostprocessor } from "./exportPostProcessor"; import { CustomSvgExporter } from "./exporter"; -import { FTAEdge, FTANode, FTAPort, FTA_EDGE_TYPE, FTA_GRAPH_TYPE, FTA_INVISIBLE_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE } from "./fta/fta-model"; -import { FTAGraphView, FTAInvisibleEdgeView, FTANodeView, PolylineArrowEdgeViewFTA } from "./fta/fta-views"; +import { DescriptionNode, FTAEdge, 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"; @@ -107,6 +107,7 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = configureModelElement(context, FTA_EDGE_TYPE, FTAEdge, PolylineArrowEdgeViewFTA); configureModelElement(context, FTA_INVISIBLE_EDGE_TYPE, FTAEdge, FTAInvisibleEdgeView); configureModelElement(context, FTA_NODE_TYPE, FTANode, FTANodeView); + configureModelElement(context, FTA_DESCRIPTION_NODE_TYPE, DescriptionNode, DescriptionNodeView); configureModelElement(context, FTA_GRAPH_TYPE, SGraph, FTAGraphView); configureModelElement(context, FTA_PORT_TYPE, FTAPort, PortView); }); diff --git a/extension/src-webview/fta/fta-model.ts b/extension/src-webview/fta/fta-model.ts index 06127cf8..6da34928 100644 --- a/extension/src-webview/fta/fta-model.ts +++ b/extension/src-webview/fta/fta-model.ts @@ -29,6 +29,7 @@ import { /* fault tree element types */ export const FTA_NODE_TYPE = "node:fta"; +export const FTA_DESCRIPTION_NODE_TYPE = "node:fta:description"; export const FTA_EDGE_TYPE = "edge:fta"; export const FTA_INVISIBLE_EDGE_TYPE = "edge:fta:invisible"; export const FTA_GRAPH_TYPE = "graph:fta"; @@ -56,6 +57,21 @@ export class FTANode extends SNode { n?: number; } +export class DescriptionNode extends SNode { + static readonly DEFAULT_FEATURES = [ + connectableFeature, + selectFeature, + layoutContainerFeature, + fadeFeature, + hoverFeedbackFeature, + popupFeature, + ]; + + name: string; + inCurrentSelectedCutSet?: boolean; + notConnectedToSelectedCutSet?: boolean; +} + /** * Edge of a fault tree. */ @@ -79,7 +95,6 @@ export enum FTNodeType { OR, KN, INHIBIT, - DESCRIPTION, PARENT, UNDEFINED, } diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index 6e27f3df..c3ace1dc 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -18,9 +18,9 @@ /** @jsx svg */ import { injectable } from 'inversify'; import { VNode } from "snabbdom"; -import { IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, svg } from 'sprotty'; -import { renderAndGate, renderOval, renderInhibitGate, renderKnGate, renderOrGate, renderRectangle } from "../views-rendering"; -import { FTAEdge, FTANode, FTAPort, FTA_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType } from './fta-model'; +import { Hoverable, IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, SShapeElement, Selectable, svg } from 'sprotty'; +import { renderAndGate, renderHorizontalLine, renderInhibitGate, renderKnGate, renderOrGate, renderOval, renderRectangle, renderVerticalLine } from "../views-rendering"; +import { DescriptionNode, FTAEdge, FTANode, FTAPort, FTA_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType } from './fta-model'; @injectable() export class PolylineArrowEdgeViewFTA extends PolylineEdgeView { @@ -45,6 +45,28 @@ export class FTAInvisibleEdgeView extends PolylineArrowEdgeViewFTA { } } +@injectable() +export class DescriptionNodeView extends RectangularNodeView { + render(node: DescriptionNode, context: RenderingContext): VNode | undefined { + const element = renderRectangle(node); + const border1 = renderHorizontalLine(node); + const border2 = renderHorizontalLine(node); + const edge = renderVerticalLine(node); + const translateBorder = `translate(0, ${Math.max(node.size.height, 0)})`; + const translateEdge = `translate(${Math.max(node.size.width / 2.0, 0)}, 0)`; + return + {edge} + {element} + {border1} + {border2} + {context.renderChildren(node)} + ; + } +} + @injectable() export class FTANodeView extends RectangularNodeView { @@ -60,7 +82,6 @@ export class FTANodeView extends RectangularNodeView { class-greyed-out={false}> {context.renderChildren(node)} ; - case FTNodeType.DESCRIPTION: case FTNodeType.TOPEVENT: element = renderRectangle(node); break; @@ -103,7 +124,10 @@ export class FTAGraphView extends SGraphView { render(model: Readonly, context: RenderingContext): VNode { if (model.children.length !== 0) { - this.highlightConnectedToCutSet(model, model.children[0] as FTANode); + const topEvent = model.children.find(node => node instanceof FTANode && node.nodeType === FTNodeType.TOPEVENT); + if (topEvent) { + this.highlightConnectedToCutSet(model, topEvent as FTANode); + } } return super.render(model, context); diff --git a/extension/src-webview/views-rendering.tsx b/extension/src-webview/views-rendering.tsx index 48fea548..0b6ae0f6 100644 --- a/extension/src-webview/views-rendering.tsx +++ b/extension/src-webview/views-rendering.tsx @@ -44,6 +44,28 @@ export function renderRectangle(node: SNode): VNode { />; } +/** + * Creates rectangle borders for {@code node} at the top and bottom. + * @param node The node that should be represented by a rectangle. + * @returns rectangle borders for {@code node} at the top and bottom. + */ +export function renderHorizontalLine(node: SNode): VNode { + return + + ; +} + +/** + * Creates a vertical line going through {@code node} in the middle. + * @param node The node for which the line should be created. + * @returns a vertical line for {@code node} going through its mid. + */ +export function renderVerticalLine(node: SNode): VNode { + return + + ; +} + /** * Creates a rounded rectangle for {@code node}. * @param node The node that should be represented by a rounded rectangle. @@ -204,8 +226,8 @@ export function renderOrGate(node: SNode): VNode { const nearBotY = botY - 5; const midY = Math.max(node.size.height, 0) / 2; const topY = 0; - const path = `M${leftX},${midY} V ${botY}` + `C ${leftX}, ${botY} ${leftX+10}, ${nearBotY} ${midX}, ${nearBotY} ${rightX-10}, ${nearBotY} ${rightX}, ${botY} ${rightX}, ${botY}` - + `V ${midY} A ${node.size.width},${node.size.height-10},${0},${0},${0},${midX},${topY} A ${node.size.width},${node.size.height-10},${0},${0},${0},${leftX},${midY} Z`; + const path = `M${leftX},${midY} V ${botY}` + `C ${leftX}, ${botY} ${leftX + 10}, ${nearBotY} ${midX}, ${nearBotY} ${rightX - 10}, ${nearBotY} ${rightX}, ${botY} ${rightX}, ${botY}` + + `V ${midY} A ${node.size.width},${node.size.height - 10},${0},${0},${0},${midX},${topY} A ${node.size.width},${node.size.height - 10},${0},${0},${0},${leftX},${midY} Z`; return From 1accdaa1760c78d56f8cf22be38338151ab27bf1 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 26 Oct 2023 16:34:26 +0200 Subject: [PATCH 09/33] adjusted layout --- .../fta/diagram/fta-diagram-generator.ts | 68 ++++++++++--------- .../fta/diagram/fta-layout-config.ts | 55 +++++++++------ extension/src-webview/css/fta-diagram.css | 4 ++ extension/src-webview/fta/fta-views.tsx | 4 +- extension/src-webview/views-rendering.tsx | 33 +++++---- 5 files changed, 90 insertions(+), 74 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index 549391df..95f92f1a 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -130,41 +130,29 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { // create edge to target if (targetId) { - // create port for the target node - const targetNode = this.idToSNode.get(targetId); - const targetPortId = idCache.uniqueId(edgeId + "_port"); - targetNode?.children?.push(this.createFTAPort(targetPortId, PortSide.NORTH)); - - let targetParentPortId: string | undefined = undefined; - // create edge from parent port to description node + let targetPortId: string | undefined = undefined; if (this.parentOfGate.has(targetId)) { + // get the port id from the parent const parent = this.parentOfGate.get(targetId); - targetParentPortId = this.parentToPort.get(parent?.id ?? "")?.id; - if (targetParentPortId) { - const betweenEdgeId = idCache.uniqueId(edgeId + "_betweenEdge"); - const descriptionId = this.descriptionOfGate.get(targetId)?.id; - if (descriptionId) { - const invisibleEdge = this.generateFTEdge( - betweenEdgeId, - targetParentPortId, - descriptionId, - FTA_INVISIBLE_EDGE_TYPE, - idCache - ); - elements.push(invisibleEdge); - } - } + targetPortId = this.parentToPort.get(parent?.id ?? "")?.id; + } else { + // create port for the target node + const targetNode = this.idToSNode.get(targetId); + targetPortId = idCache.uniqueId(edgeId + "_port"); + targetNode?.children?.push(this.createFTAPort(targetPortId, PortSide.NORTH)); } - // create edge from source to target - const e = this.generateFTEdge( - edgeId, - sourceParentPortId ?? sourcePortId, - targetParentPortId ?? targetPortId, - FTA_EDGE_TYPE, - idCache - ); - elements.push(e); + if (targetPortId) { + // create edge from source to target + const e = this.generateFTEdge( + edgeId, + sourceParentPortId ?? sourcePortId, + targetPortId, + FTA_EDGE_TYPE, + idCache + ); + elements.push(e); + } } } } @@ -223,6 +211,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { }, }; + // create invisible edge from description to gate const invisibleEdge = this.generateFTEdge( idCache.uniqueId(node.name + "InvisibleEdge"), descriptionNode.id, @@ -231,10 +220,22 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { idCache ); - // order is important to have the descriptionNode above the gateNode const parentId = idCache.uniqueId(node.name + "Parent"); const port = this.createFTAPort(idCache.uniqueId(parentId + "_port"), PortSide.NORTH); - const children: SModelElement[] = [descriptionNode, gateNode, invisibleEdge, port]; + + // create invisible edge from parent to description + const betweenEdgeId = idCache.uniqueId(node.name + "InvisibleEdge"); + const invisibleEdgeParetToDescription = this.generateFTEdge( + betweenEdgeId, + port.id, + descriptionNode.id, + FTA_INVISIBLE_EDGE_TYPE, + idCache + ); + + // order is important to have the descriptionNode above the gateNode + const children: SModelElement[] = [descriptionNode, gateNode, invisibleEdge, port, invisibleEdgeParetToDescription]; + // create invisible node that contains the desciprion and gate node const parent = { type: FTA_NODE_TYPE, @@ -254,6 +255,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { }, }; + // update maps this.idToSNode.set(descriptionNode.id, descriptionNode); this.descriptionOfGate.set(gateNode.id, descriptionNode); this.parentOfGate.set(gateNode.id, parent); diff --git a/extension/src-language-server/fta/diagram/fta-layout-config.ts b/extension/src-language-server/fta/diagram/fta-layout-config.ts index ef846ecd..dfc555de 100644 --- a/extension/src-language-server/fta/diagram/fta-layout-config.ts +++ b/extension/src-language-server/fta/diagram/fta-layout-config.ts @@ -17,54 +17,65 @@ import { LayoutOptions } from "elkjs"; import { DefaultLayoutConfigurator } from "sprotty-elk/lib/elk-layout"; -import { SGraph, SModelIndex } from "sprotty-protocol"; +import { SGraph, SNode, SModelIndex } from "sprotty-protocol"; import { FTANode, FTAPort } from "./fta-interfaces"; -import { FTA_PORT_TYPE, FTNodeType, PortSide } from "./fta-model"; +import { FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType, PortSide } from "./fta-model"; export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { protected graphOptions(_sgraph: SGraph, _index: SModelIndex): LayoutOptions { return { "org.eclipse.elk.direction": "DOWN", "org.eclipse.elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", - 'org.eclipse.elk.portConstraints': 'FIXED_SIDE' + "org.eclipse.elk.portConstraints": "FIXED_SIDE", + "org.eclipse.elk.hierarchyHandling": "INCLUDE_CHILDREN", }; } - protected nodeOptions(snode: FTANode, _index: SModelIndex): LayoutOptions | undefined { - switch (snode.nodeType) { - case FTNodeType.PARENT: - return { - "org.eclipse.elk.direction": "DOWN", - "org.eclipse.elk.padding": "[top=0.0,left=0.0,bottom=10.0,right=0.0]", - "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "0", - 'org.eclipse.elk.portConstraints': 'FIXED_SIDE' - }; - default: - return { - "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", - }; + protected nodeOptions(snode: SNode, _index: SModelIndex): LayoutOptions | undefined { + if (snode.type === FTA_NODE_TYPE) { + switch ((snode as FTANode).nodeType) { + case FTNodeType.PARENT: + return { + "org.eclipse.elk.direction": "DOWN", + "org.eclipse.elk.padding": "[top=0.0,left=0.0,bottom=10.0,right=0.0]", + "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "2", + "org.eclipse.elk.portConstraints": "FIXED_SIDE", + "org.eclipse.elk.spacing.portPort": "0.0", + "org.eclipse.elk.hierarchyHandling": "INCLUDE_CHILDREN", + }; + default: + return { + "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", + "org.eclipse.elk.spacing.portPort": "0.0", + }; + } + } else { + return { + "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", + "org.eclipse.elk.spacing.portPort": "0.0", + }; } } protected portOptions(sport: FTAPort, index: SModelIndex): LayoutOptions | undefined { if (sport.type === FTA_PORT_TYPE) { - let side = ''; + let side = ""; switch ((sport as FTAPort).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-webview/css/fta-diagram.css b/extension/src-webview/css/fta-diagram.css index 1a349cff..cb31f385 100644 --- a/extension/src-webview/css/fta-diagram.css +++ b/extension/src-webview/css/fta-diagram.css @@ -55,4 +55,8 @@ .vertical-edge { stroke: darkgray; +} + +.description-border { + stroke: grey; } \ No newline at end of file diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index c3ace1dc..36020213 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -60,8 +60,8 @@ export class DescriptionNodeView extends RectangularNodeView { class-greyed-out={node.notConnectedToSelectedCutSet}> {edge} {element} - {border1} - {border2} + {border1} + {border2} {context.renderChildren(node)} ; } diff --git a/extension/src-webview/views-rendering.tsx b/extension/src-webview/views-rendering.tsx index 0b6ae0f6..257d594d 100644 --- a/extension/src-webview/views-rendering.tsx +++ b/extension/src-webview/views-rendering.tsx @@ -219,16 +219,7 @@ export function renderAndGate(node: SNode): VNode { * @returns An Or-Gate for {@code node}. */ export function renderOrGate(node: SNode): VNode { - const leftX = 0; - const rightX = Math.max(node.size.width, 0); - const midX = rightX / 2.0; - const botY = Math.max(node.size.height, 0); - const nearBotY = botY - 5; - const midY = Math.max(node.size.height, 0) / 2; - const topY = 0; - const path = `M${leftX},${midY} V ${botY}` + `C ${leftX}, ${botY} ${leftX + 10}, ${nearBotY} ${midX}, ${nearBotY} ${rightX - 10}, ${nearBotY} ${rightX}, ${botY} ${rightX}, ${botY}` - + `V ${midY} A ${node.size.width},${node.size.height - 10},${0},${0},${0},${midX},${topY} A ${node.size.width},${node.size.height - 10},${0},${0},${0},${leftX},${midY} Z`; - + const path = createOrGate(node); return ; @@ -240,16 +231,10 @@ export function renderOrGate(node: SNode): VNode { * @returns An Kn-Gate for {@code node}. */ export function renderKnGate(node: SNode, k: number, n: number): VNode { - const leftX = 0; const rightX = Math.max(node.size.width, 0); const midX = rightX / 2.0; const botY = Math.max(node.size.height, 0); - const nearBotY = Math.max(node.size.height, 0) - (Math.max(node.size.height, 0) / 10.0); - const midY = Math.max(node.size.height, 0) / 2; - const topY = 0; - const path = `M${leftX},${midY} V ${botY}` + `C ${leftX}, ${botY} ${leftX + 10}, ${nearBotY} ${midX}, ${nearBotY} ${rightX - 10}, ${nearBotY} ${rightX}, ${botY} ${rightX}, ${botY}` - + `V ${midY} A ${node.size.width},${node.size.height - 10},${0},${0},${0},${midX},${topY} A ${node.size.width},${node.size.height - 10},${0},${0},${0},${leftX},${midY} Z`; - + const path = createOrGate(node); return ( @@ -260,6 +245,20 @@ export function renderKnGate(node: SNode, k: number, n: number): VNode { ); } +function createOrGate(node: SNode): string { + const leftX = 0; + const rightX = Math.max(node.size.width, 0); + const midX = rightX / 2.0; + const botY = Math.max(node.size.height, 0); + const nearBotY = botY - (Math.max(node.size.height, 0) / 10.0); + const midY = Math.max(node.size.height, 0) / 2; + const topY = 0; + const path = `M${leftX},${midY} V ${botY}` + `C ${leftX}, ${botY} ${leftX + 10}, ${nearBotY} ${midX}, ${nearBotY} ${rightX - 10}, ${nearBotY} ${rightX}, ${botY} ${rightX}, ${botY}` + + `V ${midY} A ${node.size.width},${node.size.height - 10},${0},${0},${0},${midX},${topY} A ${node.size.width},${node.size.height - 10},${0},${0},${0},${leftX},${midY} Z`; + + return path; +} + /** * Creates an Inhibit-Gate for {@code node}. * @param node The node that should be represented by an Inhibit-Gate. From 181cb4683b691f9e265a57bd810f5ba83fbe59c7 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 27 Oct 2023 09:30:51 +0200 Subject: [PATCH 10/33] merge outgoing edges --- .../fta/diagram/fta-diagram-generator.ts | 17 ++++++++++------- extension/src-webview/css/fta-diagram.css | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index 95f92f1a..8afba4b3 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -43,7 +43,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected parentOfGate: Map = new Map(); protected descriptionOfGate: Map = new Map(); - protected parentToPort: Map = new Map(); + protected nodeToPort: Map = new Map(); constructor(services: FtaServices) { super(services); @@ -134,12 +134,11 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { if (this.parentOfGate.has(targetId)) { // get the port id from the parent const parent = this.parentOfGate.get(targetId); - targetPortId = this.parentToPort.get(parent?.id ?? "")?.id; + targetPortId = this.nodeToPort.get(parent?.id ?? "")?.id; } else { - // create port for the target node + // get the port id from the target node const targetNode = this.idToSNode.get(targetId); - targetPortId = idCache.uniqueId(edgeId + "_port"); - targetNode?.children?.push(this.createFTAPort(targetPortId, PortSide.NORTH)); + targetPortId = this.nodeToPort.get(targetNode?.id ?? "")?.id; } if (targetPortId) { @@ -215,7 +214,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { const invisibleEdge = this.generateFTEdge( idCache.uniqueId(node.name + "InvisibleEdge"), descriptionNode.id, - gateNode.id, + this.nodeToPort.get(gateNode.id)?.id ?? gateNode.id, FTA_INVISIBLE_EDGE_TYPE, idCache ); @@ -259,7 +258,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { this.idToSNode.set(descriptionNode.id, descriptionNode); this.descriptionOfGate.set(gateNode.id, descriptionNode); this.parentOfGate.set(gateNode.id, parent); - this.parentToPort.set(parent.id, port); + this.nodeToPort.set(parent.id, port); return parent; } @@ -272,6 +271,10 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected generateFTNode(node: namedFtaElement, idCache: IdCache): FTANode { const nodeId = idCache.uniqueId(node.name.replace(" ", ""), node); const children: SModelElement[] = this.createNodeLabel(node.name, nodeId, idCache); + // one port for outgoing edges + const port = this.createFTAPort(idCache.uniqueId(nodeId + "_port"), PortSide.NORTH); + children.push(port); + this.nodeToPort.set(nodeId, port); const description = isComponent(node) || isCondition(node) ? node.description : ""; const set = this.options.getCutSet(); let includedInCutSet = set !== noCutSet.id ? set.includes(node.name) : false; diff --git a/extension/src-webview/css/fta-diagram.css b/extension/src-webview/css/fta-diagram.css index cb31f385..3f2febed 100644 --- a/extension/src-webview/css/fta-diagram.css +++ b/extension/src-webview/css/fta-diagram.css @@ -54,9 +54,9 @@ } .vertical-edge { - stroke: darkgray; + stroke: lightgray; } .description-border { - stroke: grey; + stroke: dimgray; } \ No newline at end of file From cfc9b5474f7bf4e313f3386369818a9539fcb979 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 27 Oct 2023 13:25:03 +0200 Subject: [PATCH 11/33] junction points are drawn --- .../fta/diagram/fta-interfaces.ts | 3 +- .../src-language-server/fta/fta-module.ts | 3 +- .../src-language-server/layout-engine.ts | 59 +++++++++++++++++++ extension/src-webview/fta/fta-model.ts | 4 +- extension/src-webview/fta/fta-views.tsx | 12 +++- extension/src-webview/views-rendering.tsx | 10 ++++ 6 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 extension/src-language-server/layout-engine.ts diff --git a/extension/src-language-server/fta/diagram/fta-interfaces.ts b/extension/src-language-server/fta/diagram/fta-interfaces.ts index 7cb9aceb..02acc715 100644 --- a/extension/src-language-server/fta/diagram/fta-interfaces.ts +++ b/extension/src-language-server/fta/diagram/fta-interfaces.ts @@ -15,7 +15,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { SEdge, SNode, SPort } from "sprotty-protocol"; +import { Point, SEdge, SNode, SPort } from "sprotty-protocol"; import { FTNodeType, PortSide } from "./fta-model"; /** @@ -42,6 +42,7 @@ export interface DescriptionNode extends SNode { */ export interface FTAEdge extends SEdge { notConnectedToSelectedCutSet?: boolean; + junctionPoints?: Point[]; } /** Port representing a port in the FTA graph. */ diff --git a/extension/src-language-server/fta/fta-module.ts b/extension/src-language-server/fta/fta-module.ts index 76d0b4f7..fee70524 100644 --- a/extension/src-language-server/fta/fta-module.ts +++ b/extension/src-language-server/fta/fta-module.ts @@ -30,6 +30,7 @@ import { FtaLayoutConfigurator } from "./diagram/fta-layout-config"; import { FtaScopeProvider } from "./fta-scopeProvider"; import { FtaSynthesisOptions } from "./fta-synthesis-options"; import { FtaValidationRegistry, FtaValidator } from "./fta-validator"; +import { LayoutEngine } from "../layout-engine"; /** * Declaration of custom services. @@ -66,7 +67,7 @@ export const FtaModule: Module new FtaDiagramGenerator(services), ModelLayoutEngine: (services) => - new ElkLayoutEngine( + new LayoutEngine( services.layout.ElkFactory, services.layout.ElementFilter, services.layout.LayoutConfigurator diff --git a/extension/src-language-server/layout-engine.ts b/extension/src-language-server/layout-engine.ts new file mode 100644 index 00000000..97887015 --- /dev/null +++ b/extension/src-language-server/layout-engine.ts @@ -0,0 +1,59 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2021-2022 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 { ElkExtendedEdge, ElkPrimitiveEdge } from "elkjs/lib/elk-api"; +import { ElkLayoutEngine } from "sprotty-elk/lib/elk-layout"; +import { Point, SEdge, SModelIndex } from "sprotty-protocol"; +import { FTAEdge } from "../src-webview/fta/fta-model"; +import { FTA_EDGE_TYPE } from "./fta/diagram/fta-model"; + +export class LayoutEngine extends ElkLayoutEngine { + + /** Override method to save the junctionpoints in FTAEdges*/ + protected applyEdge(sedge: SEdge, elkEdge: ElkExtendedEdge, index: SModelIndex): void { + const points: Point[] = []; + if (sedge.type === FTA_EDGE_TYPE) { + (sedge as any as FTAEdge).junctionPoints = elkEdge.junctionPoints; + } + if (elkEdge.sections && elkEdge.sections.length > 0) { + const section = elkEdge.sections[0]; + if (section.startPoint) points.push(section.startPoint); + if (section.bendPoints) points.push(...section.bendPoints); + if (section.endPoint) points.push(section.endPoint); + } else if (isPrimitiveEdge(elkEdge)) { + if (elkEdge.sourcePoint) points.push(elkEdge.sourcePoint); + if (elkEdge.bendPoints) points.push(...elkEdge.bendPoints); + if (elkEdge.targetPoint) points.push(elkEdge.targetPoint); + } + sedge.routingPoints = points; + + if (elkEdge.labels) { + elkEdge.labels.forEach((elkLabel) => { + const sLabel = elkLabel.id && index.getById(elkLabel.id); + if (sLabel) { + this.applyShape(sLabel, elkLabel, index); + } + }); + } + } +} + +function isPrimitiveEdge(edge: unknown): edge is ElkPrimitiveEdge { + return ( + typeof (edge as ElkPrimitiveEdge).source === "string" && typeof (edge as ElkPrimitiveEdge).target === "string" + ); +} diff --git a/extension/src-webview/fta/fta-model.ts b/extension/src-webview/fta/fta-model.ts index 6da34928..f17f4289 100644 --- a/extension/src-webview/fta/fta-model.ts +++ b/extension/src-webview/fta/fta-model.ts @@ -16,6 +16,7 @@ */ import { + Point, SEdge, SNode, SPort, @@ -24,7 +25,7 @@ import { hoverFeedbackFeature, layoutContainerFeature, popupFeature, - selectFeature, + selectFeature } from "sprotty"; /* fault tree element types */ @@ -77,6 +78,7 @@ export class DescriptionNode extends SNode { */ export class FTAEdge extends SEdge { notConnectedToSelectedCutSet?: boolean; + junctionPoints?: Point[]; } /** Port representing a port in the FTA graph. */ diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index 36020213..4cdd9f80 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -19,7 +19,7 @@ import { injectable } from 'inversify'; import { VNode } from "snabbdom"; import { Hoverable, IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, SShapeElement, Selectable, svg } from 'sprotty'; -import { renderAndGate, renderHorizontalLine, renderInhibitGate, renderKnGate, renderOrGate, renderOval, renderRectangle, renderVerticalLine } from "../views-rendering"; +import { renderAndGate, renderEllipse, renderHorizontalLine, renderInhibitGate, renderKnGate, renderOrGate, renderOval, renderRectangle, renderVerticalLine } from "../views-rendering"; import { DescriptionNode, FTAEdge, FTANode, FTAPort, FTA_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType } from './fta-model'; @injectable() @@ -32,8 +32,16 @@ export class PolylineArrowEdgeViewFTA extends PolylineEdgeView { const p = segments[i]; path += ` L ${p.x},${p.y}`; } + // renderings for all junction points + const junctionPointRenderings = edge.junctionPoints?.map(junctionPoint => + renderEllipse(junctionPoint.x, junctionPoint.y, 4, 4, 1) + ); + // if an FTANode is selected, the components not connected to it should fade out - return ; + return + + {...(junctionPointRenderings ?? [])} + ; } } diff --git a/extension/src-webview/views-rendering.tsx b/extension/src-webview/views-rendering.tsx index 257d594d..e8f1578f 100644 --- a/extension/src-webview/views-rendering.tsx +++ b/extension/src-webview/views-rendering.tsx @@ -32,6 +32,16 @@ export function renderOval(node: SNode): VNode { ry={Math.max(node.size.height, 0) / 2.0} />; } +export function renderEllipse(x: number | undefined, y: number | undefined, width: number, height: number, lineWidth: number): VNode { + return ; +} + /** * Creates a rectangle for {@code node}. * @param node The node that should be represented by a rectangle. From 21da3ff1ad18d50c01d985806c4c8ef36123401a Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 27 Oct 2023 14:50:41 +0200 Subject: [PATCH 12/33] label management for fta --- .../fta/diagram/fta-diagram-generator.ts | 43 ++++-- .../{ => diagram}/fta-synthesis-options.ts | 65 ++++++++- .../fta/fta-message-handler.ts | 7 +- .../src-language-server/fta/fta-module.ts | 7 +- .../src-language-server/layout-engine.ts | 20 ++- .../stpa/diagram/diagram-generator.ts | 70 +++------ .../stpa/diagram/stpa-synthesis-options.ts | 133 +++--------------- .../src-language-server/synthesis-options.ts | 97 ++++++++++++- extension/src-language-server/utils.ts | 63 ++++++++- extension/src-webview/fta/fta-views.tsx | 26 ++-- 10 files changed, 326 insertions(+), 205 deletions(-) rename extension/src-language-server/fta/{ => diagram}/fta-synthesis-options.ts (62%) 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 8afba4b3..884024ca 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -18,9 +18,9 @@ import { AstNode } from "langium"; import { GeneratorContext, IdCache, LangiumDiagramGenerator } from "langium-sprotty"; import { SLabel, SModelElement, SModelRoot, SNode } from "sprotty-protocol"; -import { Gate, ModelFTA, isComponent, isCondition, isKNGate } from "../../generated/ast"; +import { Component, Condition, Gate, ModelFTA, TopEvent, isComponent, isCondition, isKNGate } from "../../generated/ast"; +import { getDescription } from "../../utils"; import { FtaServices } from "../fta-module"; -import { FtaSynthesisOptions, noCutSet, spofsSet } from "../fta-synthesis-options"; import { namedFtaElement } from "../utils"; import { DescriptionNode, FTAEdge, FTANode, FTAPort } from "./fta-interfaces"; import { @@ -33,6 +33,7 @@ import { FTNodeType, PortSide, } from "./fta-model"; +import { FtaSynthesisOptions, noCutSet, spofsSet } from "./fta-synthesis-options"; import { getFTNodeType, getTargets } from "./utils"; export class FtaDiagramGenerator extends LangiumDiagramGenerator { @@ -50,9 +51,6 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { this.options = services.options.SynthesisOptions; } - // TODO: replace with synthesis option - protected showDescriptions = true; - /** * Generates an SGraph for the FTA model contained in {@code args}. * @param args GeneratorContext for the FTA model. @@ -189,16 +187,23 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected generateGate(node: Gate, idCache: IdCache): FTANode { const gateNode = this.generateFTNode(node, idCache); this.idToSNode.set(gateNode.id, gateNode); - if (!this.showDescriptions || node.description === undefined) { + if (!this.options.getShowGateDescriptions() || node.description === undefined) { return gateNode; } // create node for gate description const descriptionNodeId = idCache.uniqueId(node.name + "Description"); + const label = getDescription( + node.description, + this.options.getLabelManagement(), + this.options.getLabelShorteningWidth(), + descriptionNodeId, + idCache + ).reverse(); const descriptionNode: DescriptionNode = { type: FTA_DESCRIPTION_NODE_TYPE, id: descriptionNodeId, name: node.description ?? "", - children: this.createNodeLabel(node.description, descriptionNodeId, idCache), + children: label, layout: "stack", inCurrentSelectedCutSet: gateNode.inCurrentSelectedCutSet, notConnectedToSelectedCutSet: gateNode.notConnectedToSelectedCutSet, @@ -221,7 +226,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { const parentId = idCache.uniqueId(node.name + "Parent"); const port = this.createFTAPort(idCache.uniqueId(parentId + "_port"), PortSide.NORTH); - + // create invisible edge from parent to description const betweenEdgeId = idCache.uniqueId(node.name + "InvisibleEdge"); const invisibleEdgeParetToDescription = this.generateFTEdge( @@ -233,8 +238,14 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { ); // order is important to have the descriptionNode above the gateNode - const children: SModelElement[] = [descriptionNode, gateNode, invisibleEdge, port, invisibleEdgeParetToDescription]; - + const children: SModelElement[] = [ + descriptionNode, + gateNode, + invisibleEdge, + port, + invisibleEdgeParetToDescription, + ]; + // create invisible node that contains the desciprion and gate node const parent = { type: FTA_NODE_TYPE, @@ -271,10 +282,20 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected generateFTNode(node: namedFtaElement, idCache: IdCache): FTANode { const nodeId = idCache.uniqueId(node.name.replace(" ", ""), node); const children: SModelElement[] = this.createNodeLabel(node.name, nodeId, idCache); + if (this.options.getShowComponentDescriptions() && (node.$type === Component || node.$type === Condition) && node.description !== undefined) { + const label = getDescription( + node.description, + this.options.getLabelManagement(), + this.options.getLabelShorteningWidth(), + nodeId, + idCache + ); + children.push(...label.reverse()); + } // one port for outgoing edges const port = this.createFTAPort(idCache.uniqueId(nodeId + "_port"), PortSide.NORTH); children.push(port); - this.nodeToPort.set(nodeId, port); + this.nodeToPort.set(nodeId, port); const description = isComponent(node) || isCondition(node) ? node.description : ""; const set = this.options.getCutSet(); let includedInCutSet = set !== noCutSet.id ? set.includes(node.name) : false; diff --git a/extension/src-language-server/fta/fta-synthesis-options.ts b/extension/src-language-server/fta/diagram/fta-synthesis-options.ts similarity index 62% rename from extension/src-language-server/fta/fta-synthesis-options.ts rename to extension/src-language-server/fta/diagram/fta-synthesis-options.ts index 27d27d1c..d2204693 100644 --- a/extension/src-language-server/fta/fta-synthesis-options.ts +++ b/extension/src-language-server/fta/diagram/fta-synthesis-options.ts @@ -15,15 +15,65 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { DropDownOption, TransformationOptionType, ValuedSynthesisOption } from "../options/option-models"; -import { SynthesisOptions } from "../synthesis-options"; +import { DropDownOption, SynthesisOption, TransformationOptionType, ValuedSynthesisOption } from "../../options/option-models"; +import { SynthesisOptions, layoutCategory } from "../../synthesis-options"; const cutSetsID = "cutSets"; +const showGateDescriptionsID = "showGateDescriptions"; +const showComponentDescriptionsID = "showComponentDescriptions"; + +const analysisCategoryID = "analysisCategory"; export const noCutSet = { displayName: "---", id: "---" }; /* Single Point of Failure */ export const spofsSet = { displayName: "SPoFs", id: "SPoFs" }; +/** + * Category for analysis options. + */ +export const analysisCategory: SynthesisOption = { + id: analysisCategoryID, + name: "Analysis", + type: TransformationOptionType.CATEGORY, + initialValue: 0, + currentValue: 0, + values: [], +}; + +/** + * The option for the analysis category. + */ +const analysisCategoryOption: ValuedSynthesisOption = { + synthesisOption: analysisCategory, + currentValue: 0, +}; + +const showGateDescriptionsOptions: ValuedSynthesisOption = { + synthesisOption: { + id: showGateDescriptionsID, + name: "Show Gate Descriptions", + type: TransformationOptionType.CHECK, + initialValue: true, + currentValue: true, + values: [true, false], + category: layoutCategory, + }, + currentValue: true, +}; + +const showComponentDescriptionsOptions: ValuedSynthesisOption = { + synthesisOption: { + id: showComponentDescriptionsID, + name: "Show Component Descriptions", + type: TransformationOptionType.CHECK, + initialValue: false, + currentValue: false, + values: [true, false], + category: layoutCategory, + }, + currentValue: false, +}; + const cutSets: ValuedSynthesisOption = { synthesisOption: { id: cutSetsID, @@ -34,6 +84,7 @@ const cutSets: ValuedSynthesisOption = { initialValue: noCutSet.id, currentValue: noCutSet.id, values: [noCutSet], + category: analysisCategory, } as DropDownOption, currentValue: noCutSet.id, }; @@ -42,7 +93,15 @@ export class FtaSynthesisOptions extends SynthesisOptions { protected spofs: string[]; constructor() { super(); - this.options = [cutSets]; + this.options.push(...[analysisCategoryOption, cutSets, showGateDescriptionsOptions, showComponentDescriptionsOptions]); + } + + getShowGateDescriptions(): boolean { + return this.getOption(showGateDescriptionsID)?.currentValue; + } + + getShowComponentDescriptions(): boolean { + return this.getOption(showComponentDescriptionsID)?.currentValue; } /** diff --git a/extension/src-language-server/fta/fta-message-handler.ts b/extension/src-language-server/fta/fta-message-handler.ts index 933a0716..646edff4 100644 --- a/extension/src-language-server/fta/fta-message-handler.ts +++ b/extension/src-language-server/fta/fta-message-handler.ts @@ -15,15 +15,14 @@ * SPDX-License-Identifier: EPL-2.0 */ +import { AstNode } from "langium"; import { LangiumSprottySharedServices } from "langium-sprotty"; import { Connection } from "vscode-languageserver"; +import { ModelFTA } from "../generated/ast"; import { getModel } from "../utils"; -import { determineMinimalCutSets, determineCutSetsForFT } from "./analysis/fta-cutSet-calculator"; +import { determineCutSetsForFT, determineMinimalCutSets } from "./analysis/fta-cutSet-calculator"; import { FtaServices } from "./fta-module"; import { cutSetsToString } from "./utils"; -import { ModelFTA } from "../generated/ast"; -import { AstNode } from "langium"; -import { noCutSet } from "./fta-synthesis-options"; /** * Adds handlers for notifications regarding fta. diff --git a/extension/src-language-server/fta/fta-module.ts b/extension/src-language-server/fta/fta-module.ts index fee70524..5152c547 100644 --- a/extension/src-language-server/fta/fta-module.ts +++ b/extension/src-language-server/fta/fta-module.ts @@ -21,16 +21,15 @@ import { LangiumSprottyServices, SprottyDiagramServices } from "langium-sprotty" import { DefaultElementFilter, ElkFactory, - ElkLayoutEngine, IElementFilter, - ILayoutConfigurator, + ILayoutConfigurator } from "sprotty-elk/lib/elk-layout"; +import { LayoutEngine } from "../layout-engine"; import { FtaDiagramGenerator } from "./diagram/fta-diagram-generator"; import { FtaLayoutConfigurator } from "./diagram/fta-layout-config"; +import { FtaSynthesisOptions } from "./diagram/fta-synthesis-options"; import { FtaScopeProvider } from "./fta-scopeProvider"; -import { FtaSynthesisOptions } from "./fta-synthesis-options"; import { FtaValidationRegistry, FtaValidator } from "./fta-validator"; -import { LayoutEngine } from "../layout-engine"; /** * Declaration of custom services. diff --git a/extension/src-language-server/layout-engine.ts b/extension/src-language-server/layout-engine.ts index 97887015..fc4a53bf 100644 --- a/extension/src-language-server/layout-engine.ts +++ b/extension/src-language-server/layout-engine.ts @@ -15,13 +15,29 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { ElkExtendedEdge, ElkPrimitiveEdge } from "elkjs/lib/elk-api"; +import { ElkExtendedEdge, ElkNode, ElkPrimitiveEdge } from "elkjs/lib/elk-api"; import { ElkLayoutEngine } from "sprotty-elk/lib/elk-layout"; -import { Point, SEdge, SModelIndex } from "sprotty-protocol"; +import { Point, SEdge, SGraph, SModelIndex } from "sprotty-protocol"; import { FTAEdge } from "../src-webview/fta/fta-model"; import { FTA_EDGE_TYPE } from "./fta/diagram/fta-model"; export class LayoutEngine extends ElkLayoutEngine { + layout(graph: SGraph, index?: SModelIndex | undefined): SGraph | Promise { + if (this.getBasicType(graph) !== "graph") { + return graph; + } + if (!index) { + index = new SModelIndex(); + index.add(graph); + } + const elkGraph = this.transformToElk(graph, index) as ElkNode; + const debugElkGraph = JSON.stringify(elkGraph); + console.log(debugElkGraph); + return this.elk.layout(elkGraph).then((result) => { + this.applyLayout(result, index!); + return graph; + }); + } /** Override method to save the junctionpoints in FTAEdges*/ protected applyEdge(sedge: SEdge, elkEdge: ElkExtendedEdge, index: SModelIndex): void { diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 82dbea2f..a4d6461a 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -48,8 +48,10 @@ import { STPA_NODE_TYPE, STPA_PORT_TYPE, } from "./stpa-model"; -import { StpaSynthesisOptions, labelManagementValue, showLabelsValue } from "./stpa-synthesis-options"; +import { StpaSynthesisOptions, showLabelsValue } from "./stpa-synthesis-options"; import { createUCAContextDescription, getAspect, getTargets, setLevelOfCSNodes, setLevelsForSTPANodes } from "./utils"; +import { labelManagementValue } from "../../synthesis-options"; +import { getDescription } from "../../utils"; export class StpaDiagramGenerator extends LangiumDiagramGenerator { protected readonly options: StpaSynthesisOptions; @@ -296,7 +298,11 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param args GeneratorContext of the STPA model. * @returns A list of edges representing the commands. */ - protected translateCommandsToEdges(commands: VerticalEdge[], edgetype: EdgeType, args: GeneratorContext): CSEdge[] { + protected translateCommandsToEdges( + commands: VerticalEdge[], + edgetype: EdgeType, + args: GeneratorContext + ): CSEdge[] { const idCache = args.idCache; const edges: CSEdge[] = []; for (const edge of commands) { @@ -901,60 +907,16 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { idCache: IdCache, nodeDescription?: string ): SModelElement[] { - const labelManagement = this.options.getLabelManagement(); - const children: SModelElement[] = []; + let children: SModelElement[] = []; //TODO: automatic label selection - if (nodeDescription && showDescription) { - const width = this.options.getLabelShorteningWidth(); - const words = nodeDescription.split(" "); - let current = ""; - switch (labelManagement) { - case labelManagementValue.NO_LABELS: - break; - case labelManagementValue.ORIGINAL: - // show complete description in one line - children.push({ - type: "label", - id: idCache.uniqueId(nodeId + ".label"), - text: nodeDescription, - }); - break; - case labelManagementValue.TRUNCATE: - // truncate description to the set value - if (words.length > 0) { - current = words[0]; - for (let i = 1; i < words.length && current.length + words[i].length <= width; i++) { - current += " " + words[i]; - } - children.push({ - type: "label", - id: idCache.uniqueId(nodeId + ".label"), - text: current + "...", - }); - } - break; - case labelManagementValue.WRAPPING: - // wrap description to the set value - const descriptions: string[] = []; - for (const word of words) { - if (current.length + word.length >= width) { - descriptions.push(current); - current = word; - } else { - current += " " + word; - } - } - descriptions.push(current); - for (let i = descriptions.length - 1; i >= 0; i--) { - children.push({ - type: "label", - id: idCache.uniqueId(nodeId + ".label"), - text: descriptions[i], - }); - } - break; - } + children = getDescription( + nodeDescription ?? "", + this.options.getLabelManagement(), + this.options.getLabelShorteningWidth(), + nodeId, + idCache + ); } // show the name in the top line 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 111cc4a3..992f9e30 100644 --- a/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts +++ b/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts @@ -17,12 +17,11 @@ import { DropDownOption, - RangeOption, SynthesisOption, TransformationOptionType, - ValuedSynthesisOption, + ValuedSynthesisOption } from "../../options/option-models"; -import { SynthesisOptions } from "../../synthesis-options"; +import { SynthesisOptions, layoutCategory } from "../../synthesis-options"; const hierarchyID = "hierarchy"; const modelOrderID = "modelOrder"; @@ -38,35 +37,12 @@ const hideUCAsID = "hideUCAs"; const hideSafetyConstraintsID = "hideSafetyConstraints"; const showLabelsID = "showLabels"; -const labelManagementID = "labelManagement"; -const labelShorteningWidthID = "labelShorteningWidth"; -const layoutCategoryID = "layoutCategory"; const filterCategoryID = "filterCategory"; const showControlStructureID = "showControlStructure"; const showRelationshipGraphID = "showRelationshipGraph"; -/** - * Category for layout options. - */ -const layoutCategory: SynthesisOption = { - id: layoutCategoryID, - name: "Layout", - type: TransformationOptionType.CATEGORY, - initialValue: 0, - currentValue: 0, - values: [], -}; - -/** - * The option for the layout category. - */ -const layoutCategoryOption: ValuedSynthesisOption = { - synthesisOption: layoutCategory, - currentValue: 0, -}; - /** * Category for filtering options. */ @@ -307,42 +283,6 @@ const hideScenariosWithHazardsOption: ValuedSynthesisOption = { currentValue: false, }; -/** - * Slider to set the desired width of label lines. - */ -const labelShorteningWidthOption: ValuedSynthesisOption = { - synthesisOption: { - id: labelShorteningWidthID, - name: "Shortening Width", - type: TransformationOptionType.RANGE, - initialValue: 30, - currentValue: 30, - range: { first: 0, second: 100 }, - stepSize: 1, - values: [], - category: layoutCategory, - } as RangeOption, - currentValue: 30, -}; - -/** - * Option to determine the display of node labels. - * It can be original labels (whole label in one line), wrapping (label is wrapped into multiple lines), - * truncate (label is truncated) or no labels. - */ -const labelManagementOption: ValuedSynthesisOption = { - synthesisOption: { - id: labelManagementID, - name: "Node Label Management", - type: TransformationOptionType.CHOICE, - initialValue: "Wrapping", - currentValue: "Wrapping", - values: ["Original Labels", "Wrapping", "Truncate", "No Labels"], - category: layoutCategory, - }, - currentValue: "Wrapping", -}; - /** * Option to filter the node labels based on the aspect of the node. */ @@ -372,16 +312,6 @@ const showLabelsOption: ValuedSynthesisOption = { currentValue: "losses", }; -/** - * Values for general the label management. - */ -export enum labelManagementValue { - ORIGINAL, - WRAPPING, - TRUNCATE, - NO_LABELS, -} - /** * Values for filtering the node labels. */ @@ -399,29 +329,27 @@ export enum showLabelsValue { } export class StpaSynthesisOptions extends SynthesisOptions { - constructor() { super(); - this.options = [ - layoutCategoryOption, - filterCategoryOption, - hierarchicalGraphOption, - modelOrderOption, - groupingOfUCAs, - filteringOfUCAs, - hideSysConsOption, - hideRespsOption, - hideUCAsOption, - hideContConsOption, - hideScenariosOption, - hideScenariosWithHazardsOption, - hideSafetyConstraintsOption, - labelManagementOption, - labelShorteningWidthOption, - showLabelsOption, - showControlStructureOption, - showRelationshipGraphOption, - ]; + this.options.push( + ...[ + filterCategoryOption, + hierarchicalGraphOption, + modelOrderOption, + groupingOfUCAs, + filteringOfUCAs, + hideSysConsOption, + hideRespsOption, + hideUCAsOption, + hideContConsOption, + hideScenariosOption, + hideScenariosWithHazardsOption, + hideSafetyConstraintsOption, + showLabelsOption, + showControlStructureOption, + showRelationshipGraphOption, + ] + ); } getModelOrder(): boolean { @@ -455,25 +383,6 @@ export class StpaSynthesisOptions extends SynthesisOptions { return option?.currentValue; } - getLabelManagement(): labelManagementValue { - const option = this.options.find((option) => option.synthesisOption.id === labelManagementID); - switch (option?.currentValue) { - case "Original Labels": - return labelManagementValue.ORIGINAL; - case "Wrapping": - return labelManagementValue.WRAPPING; - case "Truncate": - return labelManagementValue.TRUNCATE; - case "No Labels": - return labelManagementValue.NO_LABELS; - } - return option?.currentValue; - } - - getLabelShorteningWidth(): number { - return this.getOption(labelShorteningWidthID)?.currentValue; - } - setShowRelationshipGraph(value: boolean): void { this.setOption(showRelationshipGraphID, value); } diff --git a/extension/src-language-server/synthesis-options.ts b/extension/src-language-server/synthesis-options.ts index d81c391c..cf6e151a 100644 --- a/extension/src-language-server/synthesis-options.ts +++ b/extension/src-language-server/synthesis-options.ts @@ -15,13 +15,87 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { ValuedSynthesisOption } from "./options/option-models"; +import { RangeOption, SynthesisOption, TransformationOptionType, ValuedSynthesisOption } from "./options/option-models"; + +const labelManagementID = "labelManagement"; +const labelShorteningWidthID = "labelShorteningWidth"; + +const layoutCategoryID = "layoutCategory"; + +/** + * Category for layout options. + */ +export const layoutCategory: SynthesisOption = { + id: layoutCategoryID, + name: "Layout", + type: TransformationOptionType.CATEGORY, + initialValue: 0, + currentValue: 0, + values: [], +}; + +/** + * The option for the layout category. + */ +const layoutCategoryOption: ValuedSynthesisOption = { + synthesisOption: layoutCategory, + currentValue: 0, +}; + +/** + * Slider to set the desired width of label lines. + */ +const labelShorteningWidthOption: ValuedSynthesisOption = { + synthesisOption: { + id: labelShorteningWidthID, + name: "Shortening Width", + type: TransformationOptionType.RANGE, + initialValue: 30, + currentValue: 30, + range: { first: 0, second: 100 }, + stepSize: 1, + values: [], + category: layoutCategory, + } as RangeOption, + currentValue: 30, +}; + +/** + * Option to determine the display of node labels. + * It can be original labels (whole label in one line), wrapping (label is wrapped into multiple lines), + * truncate (label is truncated) or no labels. + */ +const labelManagementOption: ValuedSynthesisOption = { + synthesisOption: { + id: labelManagementID, + name: "Node Label Management", + type: TransformationOptionType.CHOICE, + initialValue: "Wrapping", + currentValue: "Wrapping", + values: ["Original Labels", "Wrapping", "Truncate", "No Labels"], + category: layoutCategory, + }, + currentValue: "Wrapping", +}; + +/** + * Values for general the label management. + */ +export enum labelManagementValue { + ORIGINAL, + WRAPPING, + TRUNCATE, + NO_LABELS, +} export class SynthesisOptions { protected options: ValuedSynthesisOption[]; constructor() { - this.options = []; + this.options = [ + layoutCategoryOption, + labelManagementOption, + labelShorteningWidthOption,]; } getSynthesisOptions(): ValuedSynthesisOption[] { @@ -32,4 +106,23 @@ export class SynthesisOptions { const option = this.options.find((option) => option.synthesisOption.id === id); return option; } + + getLabelManagement(): labelManagementValue { + const option = this.options.find((option) => option.synthesisOption.id === labelManagementID); + switch (option?.currentValue) { + case "Original Labels": + return labelManagementValue.ORIGINAL; + case "Wrapping": + return labelManagementValue.WRAPPING; + case "Truncate": + return labelManagementValue.TRUNCATE; + case "No Labels": + return labelManagementValue.NO_LABELS; + } + return option?.currentValue; + } + + getLabelShorteningWidth(): number { + return this.getOption(labelShorteningWidthID)?.currentValue; + } } \ No newline at end of file diff --git a/extension/src-language-server/utils.ts b/extension/src-language-server/utils.ts index 69fcc52e..d0edbce4 100644 --- a/extension/src-language-server/utils.ts +++ b/extension/src-language-server/utils.ts @@ -16,8 +16,10 @@ */ import { AstNode, LangiumSharedServices } from "langium"; -import { LangiumSprottySharedServices } from "langium-sprotty"; +import { IdCache, LangiumSprottySharedServices } from "langium-sprotty"; +import { SLabel } from "sprotty-protocol"; import { URI } from "vscode-uri"; +import { labelManagementValue } from "./synthesis-options"; /** * Determines the model for {@code uri}. @@ -33,3 +35,62 @@ export async function getModel( const currentDoc = textDocuments.getOrCreateDocument(URI.parse(uri)); return currentDoc.parseResult.value; } + +export function getDescription( + description: string, + labelManagement: labelManagementValue, + labelWidth: number, + nodeId: string, + idCache: IdCache +): SLabel[] { + const labels: SLabel[] = []; + const words = description.split(" "); + let current = ""; + switch (labelManagement) { + case labelManagementValue.NO_LABELS: + break; + case labelManagementValue.ORIGINAL: + // show complete description in one line + labels.push({ + type: "label", + id: idCache.uniqueId(nodeId + ".label"), + text: description, + }); + break; + case labelManagementValue.TRUNCATE: + // truncate description to the set value + if (words.length > 0) { + current = words[0]; + for (let i = 1; i < words.length && current.length + words[i].length <= labelWidth; i++) { + current += " " + words[i]; + } + labels.push({ + type: "label", + id: idCache.uniqueId(nodeId + ".label"), + text: current + "...", + }); + } + break; + case labelManagementValue.WRAPPING: + // wrap description to the set value + const descriptions: string[] = []; + for (const word of words) { + if (current.length + word.length >= labelWidth) { + descriptions.push(current); + current = word; + } else { + current += " " + word; + } + } + descriptions.push(current); + for (let i = descriptions.length - 1; i >= 0; i--) { + labels.push({ + type: "label", + id: idCache.uniqueId(nodeId + ".label"), + text: descriptions[i], + }); + } + break; + } + return labels; +} diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index 4cdd9f80..40e2b577 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -146,18 +146,20 @@ export class FTAGraphView extends SGraphView { const edge = model.children.find(child => child.type === FTA_EDGE_TYPE && (child as FTAEdge).sourceId === port.id) as FTAEdge; if (edge) { edge.notConnectedToSelectedCutSet = true; - const target = (edge.target as FTAPort).parent as FTANode; - // handle successor nodes - this.highlightConnectedToCutSet(model, target); - // handle current node - if (!target.notConnectedToSelectedCutSet) { - currentNode.notConnectedToSelectedCutSet = false; - edge.notConnectedToSelectedCutSet = false; - } - // handle edges in parents - if (currentNode.nodeType === FTNodeType.PARENT) { - const innerEdge = currentNode.children.find(child => child.type === FTA_EDGE_TYPE && (child as FTAEdge).targetId === edge.sourceId) as FTAEdge; - innerEdge.notConnectedToSelectedCutSet = edge.notConnectedToSelectedCutSet; + if (edge.target instanceof FTAPort) { + const target = (edge.target as FTAPort).parent as FTANode; + // handle successor nodes + this.highlightConnectedToCutSet(model, target); + // handle current node + if (!target.notConnectedToSelectedCutSet) { + currentNode.notConnectedToSelectedCutSet = false; + edge.notConnectedToSelectedCutSet = false; + } + // handle edges in parents + if (currentNode.nodeType === FTNodeType.PARENT) { + const innerEdge = currentNode.children.find(child => child.type === FTA_EDGE_TYPE && (child as FTAEdge).targetId === edge.sourceId) as FTAEdge; + innerEdge.notConnectedToSelectedCutSet = edge.notConnectedToSelectedCutSet; + } } } } From 9c50ec30fbc49670ed72392756fefbae5942c6b6 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 27 Oct 2023 15:03:29 +0200 Subject: [PATCH 13/33] fixed node size for long labels --- .../fta/diagram/fta-layout-config.ts | 48 ++++++++++--------- extension/src-language-server/utils.ts | 6 +-- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-layout-config.ts b/extension/src-language-server/fta/diagram/fta-layout-config.ts index dfc555de..5e7ec03c 100644 --- a/extension/src-language-server/fta/diagram/fta-layout-config.ts +++ b/extension/src-language-server/fta/diagram/fta-layout-config.ts @@ -19,7 +19,7 @@ import { LayoutOptions } from "elkjs"; import { DefaultLayoutConfigurator } from "sprotty-elk/lib/elk-layout"; import { SGraph, SNode, SModelIndex } from "sprotty-protocol"; import { FTANode, FTAPort } from "./fta-interfaces"; -import { FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType, PortSide } from "./fta-model"; +import { FTA_DESCRIPTION_NODE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType, PortSide } from "./fta-model"; export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { protected graphOptions(_sgraph: SGraph, _index: SModelIndex): LayoutOptions { @@ -32,28 +32,30 @@ export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { } protected nodeOptions(snode: SNode, _index: SModelIndex): LayoutOptions | undefined { - if (snode.type === FTA_NODE_TYPE) { - switch ((snode as FTANode).nodeType) { - case FTNodeType.PARENT: - return { - "org.eclipse.elk.direction": "DOWN", - "org.eclipse.elk.padding": "[top=0.0,left=0.0,bottom=10.0,right=0.0]", - "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "2", - "org.eclipse.elk.portConstraints": "FIXED_SIDE", - "org.eclipse.elk.spacing.portPort": "0.0", - "org.eclipse.elk.hierarchyHandling": "INCLUDE_CHILDREN", - }; - default: - return { - "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", - "org.eclipse.elk.spacing.portPort": "0.0", - }; - } - } else { - return { - "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", - "org.eclipse.elk.spacing.portPort": "0.0", - }; + switch (snode.type) { + case FTA_NODE_TYPE: + switch ((snode as FTANode).nodeType) { + case FTNodeType.PARENT: + return { + "org.eclipse.elk.direction": "DOWN", + "org.eclipse.elk.padding": "[top=0.0,left=0.0,bottom=10.0,right=0.0]", + "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "2", + "org.eclipse.elk.portConstraints": "FIXED_SIDE", + "org.eclipse.elk.spacing.portPort": "0.0", + "org.eclipse.elk.hierarchyHandling": "INCLUDE_CHILDREN", + }; + default: + return { + "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", + "org.eclipse.elk.spacing.portPort": "0.0", + }; + } + case FTA_DESCRIPTION_NODE_TYPE: + return { + "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", + "org.eclipse.elk.spacing.portPort": "0.0", + "org.eclipse.elk.nodeSize.constraints": "NODE_LABELS", + }; } } diff --git a/extension/src-language-server/utils.ts b/extension/src-language-server/utils.ts index d0edbce4..089928cd 100644 --- a/extension/src-language-server/utils.ts +++ b/extension/src-language-server/utils.ts @@ -53,7 +53,7 @@ export function getDescription( // show complete description in one line labels.push({ type: "label", - id: idCache.uniqueId(nodeId + ".label"), + id: idCache.uniqueId(nodeId + "_label"), text: description, }); break; @@ -66,7 +66,7 @@ export function getDescription( } labels.push({ type: "label", - id: idCache.uniqueId(nodeId + ".label"), + id: idCache.uniqueId(nodeId + "_label"), text: current + "...", }); } @@ -86,7 +86,7 @@ export function getDescription( for (let i = descriptions.length - 1; i >= 0; i--) { labels.push({ type: "label", - id: idCache.uniqueId(nodeId + ".label"), + id: idCache.uniqueId(nodeId + "_label"), text: descriptions[i], }); } From 641eff6cb3218165fb1b73c12437e9406fc376fc Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 2 Nov 2023 14:55:46 +0100 Subject: [PATCH 14/33] adjusted fta visualization --- .../fta/analysis/fta-cutSet-calculator.ts | 14 +++++++++++--- .../fta/diagram/fta-diagram-generator.ts | 8 ++++++-- .../fta/diagram/fta-interfaces.ts | 1 + .../fta/diagram/fta-layout-config.ts | 10 ++++++++++ .../stpa/diagram/diagram-generator.ts | 11 +++++------ extension/src-webview/css/fta-diagram.css | 4 ++++ extension/src-webview/fta/fta-model.ts | 1 + extension/src-webview/fta/fta-views.tsx | 14 ++++++-------- extension/src-webview/views-rendering.tsx | 6 ++++-- yarn.lock | 8 ++++---- 10 files changed, 52 insertions(+), 25 deletions(-) diff --git a/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts b/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts index d76ca19b..419e6a9e 100644 --- a/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts +++ b/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts @@ -29,6 +29,8 @@ import { } from "../../generated/ast"; import { namedFtaElement } from "../utils"; +export let topOfAnalysis: string | undefined; + /** * Determines the minimal cut sets for the fault tree constructured by {@code allNodes}. * @param allNodes All nodes in the fault tree. @@ -69,9 +71,10 @@ function checkMinimalCutSet(cutSet: Set, allCutSets: Set[] { +export function determineCutSetsForFT(allNodes: AstNode[], startNode?: namedFtaElement): Set[] { /* Idea: Start from the top event. Get the only child of top event (will always be only one) as our starting node. @@ -79,8 +82,13 @@ export function determineCutSetsForFT(allNodes: AstNode[]): Set In the evaluation we check if the child has children too and do the same recursively until the children are components. Depending on the type of the node process the results of the children differently. */ - - const startNode = getChildOfTopEvent(allNodes); + + topOfAnalysis = startNode?.name; + if (!startNode) { + topOfAnalysis = (allNodes.find((node) => isTopEvent(node)) as namedFtaElement).name; + // if no start node is given, the top event is used as start node + startNode = getChildOfTopEvent(allNodes); + } if (startNode) { // determine the cut sets of the Fault Tree return determineCutSetsForGate(startNode, allNodes); 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 884024ca..acd5585c 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -35,6 +35,7 @@ import { } from "./fta-model"; import { FtaSynthesisOptions, noCutSet, spofsSet } from "./fta-synthesis-options"; import { getFTNodeType, getTargets } from "./utils"; +import { topOfAnalysis } from "../analysis/fta-cutSet-calculator"; export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected readonly options: FtaSynthesisOptions; @@ -314,7 +315,8 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { description, children, includedInCutSet, - notConnected + notConnected, + topOfAnalysis === node.name && this.options.getCutSet() !== noCutSet.id ); this.idToSNode.set(nodeId, ftNode); @@ -333,7 +335,8 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { description: string, children: SModelElement[], includedInCutSet: boolean | undefined, - notConnected: boolean | undefined + notConnected: boolean | undefined, + topOfAnalysis: boolean | undefined ): FTANode { return { type: FTA_NODE_TYPE, @@ -345,6 +348,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { layout: "stack", inCurrentSelectedCutSet: includedInCutSet, notConnectedToSelectedCutSet: notConnected, + topOfAnalysis: topOfAnalysis, layoutOptions: { paddingTop: 10.0, paddingBottom: 10.0, diff --git a/extension/src-language-server/fta/diagram/fta-interfaces.ts b/extension/src-language-server/fta/diagram/fta-interfaces.ts index 02acc715..cb2722f5 100644 --- a/extension/src-language-server/fta/diagram/fta-interfaces.ts +++ b/extension/src-language-server/fta/diagram/fta-interfaces.ts @@ -25,6 +25,7 @@ export interface FTANode extends SNode { name: string; nodeType: FTNodeType; description: string; + topOfAnalysis?: boolean; inCurrentSelectedCutSet?: boolean; notConnectedToSelectedCutSet?: boolean; k?: number; diff --git a/extension/src-language-server/fta/diagram/fta-layout-config.ts b/extension/src-language-server/fta/diagram/fta-layout-config.ts index 5e7ec03c..6f7c8f45 100644 --- a/extension/src-language-server/fta/diagram/fta-layout-config.ts +++ b/extension/src-language-server/fta/diagram/fta-layout-config.ts @@ -28,6 +28,7 @@ export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { "org.eclipse.elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", "org.eclipse.elk.portConstraints": "FIXED_SIDE", "org.eclipse.elk.hierarchyHandling": "INCLUDE_CHILDREN", + "org.eclipse.elk.spacing.portPort": "0.0", }; } @@ -44,6 +45,15 @@ export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { "org.eclipse.elk.spacing.portPort": "0.0", "org.eclipse.elk.hierarchyHandling": "INCLUDE_CHILDREN", }; + case FTNodeType.COMPONENT: + case FTNodeType.CONDITION: + return { + "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", + "org.eclipse.elk.spacing.portPort": "0.0", + "org.eclipse.elk.nodeSize.constraints": "MINIMUM_SIZE, NODE_LABELS", + "org.eclipse.elk.nodeSize.minimum": "(30, 30)", + "org.eclipse.elk.spacing.labelNode": "20.0", + }; default: return { "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index a4d6461a..a113b46d 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -31,6 +31,7 @@ import { isSystemConstraint, isUCA, } from "../../generated/ast"; +import { getDescription } from "../../utils"; import { StpaServices } from "../stpa-module"; import { collectElementsWithSubComps, leafElement } from "../utils"; import { filterModel } from "./filtering"; @@ -50,8 +51,6 @@ import { } from "./stpa-model"; import { StpaSynthesisOptions, showLabelsValue } from "./stpa-synthesis-options"; import { createUCAContextDescription, getAspect, getTargets, setLevelOfCSNodes, setLevelsForSTPANodes } from "./utils"; -import { labelManagementValue } from "../../synthesis-options"; -import { getDescription } from "../../utils"; export class StpaDiagramGenerator extends LangiumDiagramGenerator { protected readonly options: StpaSynthesisOptions; @@ -88,7 +87,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // subcomponents have edges to the parent const hazards = collectElementsWithSubComps(filteredModel.hazards); const sysCons = collectElementsWithSubComps(filteredModel.systemLevelConstraints); - stpaChildren = stpaChildren.concat([ + stpaChildren = stpaChildren?.concat([ ...hazards .map((sh) => this.generateAspectWithEdges( @@ -110,7 +109,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { ]); } else { // subcomponents are contained in the parent - stpaChildren = stpaChildren.concat([ + stpaChildren = stpaChildren?.concat([ ...filteredModel.hazards ?.map((h) => this.generateAspectWithEdges( @@ -134,7 +133,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { .flat(2), ]); } - stpaChildren = stpaChildren.concat([ + stpaChildren = stpaChildren?.concat([ ...filteredModel.responsibilities ?.map((r) => r.responsiblitiesForOneSystem.map((resp) => @@ -201,7 +200,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // filtering the nodes of the STPA graph const stpaNodes: STPANode[] = []; - for (const node of stpaChildren) { + for (const node of stpaChildren ?? []) { if (node.type === STPA_NODE_TYPE) { stpaNodes.push(node as STPANode); } diff --git a/extension/src-webview/css/fta-diagram.css b/extension/src-webview/css/fta-diagram.css index 3f2febed..5ae8c894 100644 --- a/extension/src-webview/css/fta-diagram.css +++ b/extension/src-webview/css/fta-diagram.css @@ -59,4 +59,8 @@ .description-border { stroke: dimgray; +} + +.top-event { + stroke: dodgerblue; } \ No newline at end of file diff --git a/extension/src-webview/fta/fta-model.ts b/extension/src-webview/fta/fta-model.ts index f17f4289..f8b937a4 100644 --- a/extension/src-webview/fta/fta-model.ts +++ b/extension/src-webview/fta/fta-model.ts @@ -52,6 +52,7 @@ export class FTANode extends SNode { name: string; nodeType: FTNodeType = FTNodeType.UNDEFINED; description: string = ""; + topOfAnalysis?: boolean; inCurrentSelectedCutSet?: boolean; notConnectedToSelectedCutSet?: boolean; k?: number; diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index 40e2b577..b5080a37 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -19,7 +19,7 @@ import { injectable } from 'inversify'; import { VNode } from "snabbdom"; import { Hoverable, IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, SShapeElement, Selectable, svg } from 'sprotty'; -import { renderAndGate, renderEllipse, renderHorizontalLine, renderInhibitGate, renderKnGate, renderOrGate, renderOval, renderRectangle, renderVerticalLine } from "../views-rendering"; +import { renderAndGate, renderEllipse, renderHorizontalLine, renderInhibitGate, renderKnGate, renderOrGate, renderOval, renderRectangle, renderRoundedRectangle, renderVerticalLine } from "../views-rendering"; import { DescriptionNode, FTAEdge, FTANode, FTAPort, FTA_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType } from './fta-model'; @injectable() @@ -38,8 +38,8 @@ export class PolylineArrowEdgeViewFTA extends PolylineEdgeView { ); // if an FTANode is selected, the components not connected to it should fade out - return - + return + {...(junctionPointRenderings ?? [])} ; } @@ -93,11 +93,9 @@ export class FTANodeView extends RectangularNodeView { case FTNodeType.TOPEVENT: element = renderRectangle(node); break; - case (FTNodeType.COMPONENT || FTNodeType.CONDITION): - element = renderOval(node); - break; + case FTNodeType.COMPONENT: case FTNodeType.CONDITION: - element = renderOval(node); + element = renderRoundedRectangle(node, 15, 15); break; case FTNodeType.AND: element = renderAndGate(node); @@ -121,7 +119,7 @@ export class FTANodeView extends RectangularNodeView { class-fta-node={true} class-mouseover={node.hoverFeedback} class-greyed-out={node.notConnectedToSelectedCutSet}> - {element} + {element} {context.renderChildren(node)} ; } diff --git a/extension/src-webview/views-rendering.tsx b/extension/src-webview/views-rendering.tsx index e8f1578f..2849fe9e 100644 --- a/extension/src-webview/views-rendering.tsx +++ b/extension/src-webview/views-rendering.tsx @@ -79,12 +79,14 @@ export function renderVerticalLine(node: SNode): VNode { /** * Creates a rounded rectangle for {@code node}. * @param node The node that should be represented by a rounded rectangle. + * @param rx The x-radius of the rounded corners. + * @param ry The y-radius of the rounded corners. * @returns A rounded rectangle for {@code node}. */ -export function renderRoundedRectangle(node: SNode): VNode { +export function renderRoundedRectangle(node: SNode, rx = 5, ry = 5): VNode { return ; } diff --git a/yarn.lock b/yarn.lock index 0c932d95..5f0fa116 100644 --- a/yarn.lock +++ b/yarn.lock @@ -238,10 +238,10 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@kieler/table-webview@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@kieler/table-webview/-/table-webview-0.0.4.tgz#c68a0652423a3c5f74a635fc5432e1430f758ee3" - integrity sha512-ZUAdX8dUCq72UdpFJz61bPX8eoJqnRuiJBOIFgOb+NqUke13zo4QmkeapmgA4zSKFBx5GC6nhSDQvVr9CMD4AQ== +"@kieler/table-webview@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@kieler/table-webview/-/table-webview-0.0.3.tgz#33199d9b0d8cd88d0aad6d4230617b94942482aa" + integrity sha512-XiDfn/MwHzVEpXLWC5DT6Ysg/5Zke3GlbtjBDDPRD1mLFXIekOCxkGYAKu068djqSAg3hsoiIhwLwWBfm48VNQ== dependencies: "@types/vscode" "^1.56.0" reflect-metadata "^0.1.13" From 185c64a7f50e39e8a7c23f91033ec22c08f334d0 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 2 Nov 2023 14:56:05 +0100 Subject: [PATCH 15/33] model order for fta --- .../fta/diagram/fta-diagram-generator.ts | 7 ++- .../fta/diagram/fta-interfaces.ts | 6 +- .../fta/diagram/fta-layout-config.ts | 58 +++++++++---------- .../stpa/diagram/stpa-synthesis-options.ts | 22 ------- .../src-language-server/synthesis-options.ts | 24 +++++++- extension/src-webview/di.config.ts | 4 +- extension/src-webview/fta/fta-model.ts | 5 ++ extension/src-webview/fta/fta-views.tsx | 4 +- 8 files changed, 71 insertions(+), 59 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index acd5585c..d71ff540 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -22,7 +22,7 @@ import { Component, Condition, Gate, ModelFTA, TopEvent, isComponent, isConditio import { getDescription } from "../../utils"; import { FtaServices } from "../fta-module"; import { namedFtaElement } from "../utils"; -import { DescriptionNode, FTAEdge, FTANode, FTAPort } from "./fta-interfaces"; +import { DescriptionNode, FTAEdge, FTAGraph, FTANode, FTAPort } from "./fta-interfaces"; import { FTA_DESCRIPTION_NODE_TYPE, FTA_EDGE_TYPE, @@ -82,11 +82,14 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { ); } - return { + const graph: FTAGraph = { type: FTA_GRAPH_TYPE, id: "root", children: ftaChildren, }; + graph.modelOrder = this.options.getModelOrder(); + + return graph; } /** diff --git a/extension/src-language-server/fta/diagram/fta-interfaces.ts b/extension/src-language-server/fta/diagram/fta-interfaces.ts index cb2722f5..3c799b3f 100644 --- a/extension/src-language-server/fta/diagram/fta-interfaces.ts +++ b/extension/src-language-server/fta/diagram/fta-interfaces.ts @@ -15,7 +15,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { Point, SEdge, SNode, SPort } from "sprotty-protocol"; +import { Point, SEdge, SGraph, SNode, SPort } from "sprotty-protocol"; import { FTNodeType, PortSide } from "./fta-model"; /** @@ -32,6 +32,10 @@ export interface FTANode extends SNode { n?: number; } +export interface FTAGraph extends SGraph { + modelOrder?: boolean; +} + export interface DescriptionNode extends SNode { name: string; inCurrentSelectedCutSet?: boolean; diff --git a/extension/src-language-server/fta/diagram/fta-layout-config.ts b/extension/src-language-server/fta/diagram/fta-layout-config.ts index 6f7c8f45..55be63e6 100644 --- a/extension/src-language-server/fta/diagram/fta-layout-config.ts +++ b/extension/src-language-server/fta/diagram/fta-layout-config.ts @@ -17,56 +17,56 @@ import { LayoutOptions } from "elkjs"; import { DefaultLayoutConfigurator } from "sprotty-elk/lib/elk-layout"; -import { SGraph, SNode, SModelIndex } from "sprotty-protocol"; -import { FTANode, FTAPort } from "./fta-interfaces"; +import { SModelIndex, SNode } from "sprotty-protocol"; +import { FTAGraph, FTANode, FTAPort } from "./fta-interfaces"; import { FTA_DESCRIPTION_NODE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType, PortSide } from "./fta-model"; export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { - protected graphOptions(_sgraph: SGraph, _index: SModelIndex): LayoutOptions { - return { + protected graphOptions(sgraph: FTAGraph, _index: SModelIndex): LayoutOptions { + const options: LayoutOptions = { "org.eclipse.elk.direction": "DOWN", "org.eclipse.elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", "org.eclipse.elk.portConstraints": "FIXED_SIDE", "org.eclipse.elk.hierarchyHandling": "INCLUDE_CHILDREN", "org.eclipse.elk.spacing.portPort": "0.0", }; + + if (sgraph.modelOrder) { + options["org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeOrdering"] = "NODES_AND_EDGES"; + options["org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder"] = "true"; + options["org.eclipse.elk.separateConnectedComponents"] = "false"; + } + return options; } protected nodeOptions(snode: SNode, _index: SModelIndex): LayoutOptions | undefined { + const options: LayoutOptions = { + "org.eclipse.elk.portConstraints": "FIXED_SIDE", + "org.eclipse.elk.spacing.portPort": "0.0", + "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", + }; switch (snode.type) { case FTA_NODE_TYPE: switch ((snode as FTANode).nodeType) { case FTNodeType.PARENT: - return { - "org.eclipse.elk.direction": "DOWN", - "org.eclipse.elk.padding": "[top=0.0,left=0.0,bottom=10.0,right=0.0]", - "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "2", - "org.eclipse.elk.portConstraints": "FIXED_SIDE", - "org.eclipse.elk.spacing.portPort": "0.0", - "org.eclipse.elk.hierarchyHandling": "INCLUDE_CHILDREN", - }; + options["org.eclipse.elk.direction"] = "DOWN"; + options["org.eclipse.elk.padding"] = "[top=0.0,left=0.0,bottom=10.0,right=0.0]"; + options["org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers"] = "2"; + options["org.eclipse.elk.hierarchyHandling"] = "INCLUDE_CHILDREN"; + break; case FTNodeType.COMPONENT: case FTNodeType.CONDITION: - return { - "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", - "org.eclipse.elk.spacing.portPort": "0.0", - "org.eclipse.elk.nodeSize.constraints": "MINIMUM_SIZE, NODE_LABELS", - "org.eclipse.elk.nodeSize.minimum": "(30, 30)", - "org.eclipse.elk.spacing.labelNode": "20.0", - }; - default: - return { - "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", - "org.eclipse.elk.spacing.portPort": "0.0", - }; + options["org.eclipse.elk.nodeSize.constraints"] = "MINIMUM_SIZE, NODE_LABELS"; + options["org.eclipse.elk.nodeSize.minimum"] = "(30, 30)"; + options["org.eclipse.elk.spacing.labelNode"] = "20.0"; + break; } + break; case FTA_DESCRIPTION_NODE_TYPE: - return { - "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", - "org.eclipse.elk.spacing.portPort": "0.0", - "org.eclipse.elk.nodeSize.constraints": "NODE_LABELS", - }; + options["org.eclipse.elk.nodeSize.constraints"] = "NODE_LABELS"; + break; } + return options; } protected portOptions(sport: FTAPort, index: SModelIndex): LayoutOptions | undefined { 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 992f9e30..20002df7 100644 --- a/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts +++ b/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts @@ -24,7 +24,6 @@ import { import { SynthesisOptions, layoutCategory } from "../../synthesis-options"; const hierarchyID = "hierarchy"; -const modelOrderID = "modelOrder"; const groupingUCAsID = "groupingUCAs"; export const filteringUCAsID = "filteringUCAs"; @@ -143,22 +142,6 @@ const hierarchicalGraphOption: ValuedSynthesisOption = { currentValue: true, }; -/** - * Boolean option to toggle model order. - */ -const modelOrderOption: ValuedSynthesisOption = { - synthesisOption: { - id: modelOrderID, - name: "Model Order", - type: TransformationOptionType.CHECK, - initialValue: true, - currentValue: true, - values: [true, false], - category: layoutCategory, - }, - currentValue: true, -}; - /** * Values for the grouping of UCAs. */ @@ -335,7 +318,6 @@ export class StpaSynthesisOptions extends SynthesisOptions { ...[ filterCategoryOption, hierarchicalGraphOption, - modelOrderOption, groupingOfUCAs, filteringOfUCAs, hideSysConsOption, @@ -352,10 +334,6 @@ export class StpaSynthesisOptions extends SynthesisOptions { ); } - getModelOrder(): boolean { - return this.getOption(modelOrderID)?.currentValue; - } - getShowLabels(): showLabelsValue { const option = this.getOption(showLabelsID); switch (option?.currentValue) { diff --git a/extension/src-language-server/synthesis-options.ts b/extension/src-language-server/synthesis-options.ts index cf6e151a..fffa9ff8 100644 --- a/extension/src-language-server/synthesis-options.ts +++ b/extension/src-language-server/synthesis-options.ts @@ -19,6 +19,7 @@ import { RangeOption, SynthesisOption, TransformationOptionType, ValuedSynthesis const labelManagementID = "labelManagement"; const labelShorteningWidthID = "labelShorteningWidth"; +const modelOrderID = "modelOrder"; const layoutCategoryID = "layoutCategory"; @@ -78,6 +79,22 @@ const labelManagementOption: ValuedSynthesisOption = { currentValue: "Wrapping", }; +/** + * Boolean option to toggle model order. + */ +const modelOrderOption: ValuedSynthesisOption = { + synthesisOption: { + id: modelOrderID, + name: "Model Order", + type: TransformationOptionType.CHECK, + initialValue: true, + currentValue: true, + values: [true, false], + category: layoutCategory, + }, + currentValue: true, +}; + /** * Values for general the label management. */ @@ -95,7 +112,8 @@ export class SynthesisOptions { this.options = [ layoutCategoryOption, labelManagementOption, - labelShorteningWidthOption,]; + labelShorteningWidthOption, + modelOrderOption]; } getSynthesisOptions(): ValuedSynthesisOption[] { @@ -125,4 +143,8 @@ export class SynthesisOptions { getLabelShorteningWidth(): number { return this.getOption(labelShorteningWidthID)?.currentValue; } + + getModelOrder(): boolean { + return this.getOption(modelOrderID)?.currentValue; + } } \ No newline at end of file diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index 538f61c3..d1b40cfa 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -40,7 +40,7 @@ import { import { SvgCommand } from "./actions"; import { SvgPostprocessor } from "./exportPostProcessor"; import { CustomSvgExporter } from "./exporter"; -import { DescriptionNode, FTAEdge, 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 { 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"; @@ -108,7 +108,7 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = configureModelElement(context, FTA_INVISIBLE_EDGE_TYPE, FTAEdge, FTAInvisibleEdgeView); configureModelElement(context, FTA_NODE_TYPE, FTANode, FTANodeView); configureModelElement(context, FTA_DESCRIPTION_NODE_TYPE, DescriptionNode, DescriptionNodeView); - configureModelElement(context, FTA_GRAPH_TYPE, SGraph, FTAGraphView); + configureModelElement(context, FTA_GRAPH_TYPE, FTAGraph, FTAGraphView); configureModelElement(context, FTA_PORT_TYPE, FTAPort, PortView); }); diff --git a/extension/src-webview/fta/fta-model.ts b/extension/src-webview/fta/fta-model.ts index f8b937a4..bdb7c4b4 100644 --- a/extension/src-webview/fta/fta-model.ts +++ b/extension/src-webview/fta/fta-model.ts @@ -18,6 +18,7 @@ import { Point, SEdge, + SGraph, SNode, SPort, connectableFeature, @@ -59,6 +60,10 @@ export class FTANode extends SNode { n?: number; } +export class FTAGraph extends SGraph { + modelOrder?: boolean; +} + export class DescriptionNode extends SNode { static readonly DEFAULT_FEATURES = [ connectableFeature, diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index b5080a37..8a95c0d2 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -20,7 +20,7 @@ import { injectable } from 'inversify'; import { VNode } from "snabbdom"; import { Hoverable, IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, SShapeElement, Selectable, svg } from 'sprotty'; import { renderAndGate, renderEllipse, renderHorizontalLine, renderInhibitGate, renderKnGate, renderOrGate, renderOval, renderRectangle, renderRoundedRectangle, renderVerticalLine } from "../views-rendering"; -import { DescriptionNode, FTAEdge, FTANode, FTAPort, FTA_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType } from './fta-model'; +import { DescriptionNode, FTAEdge, FTAGraph, FTANode, FTAPort, FTA_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType } from './fta-model'; @injectable() export class PolylineArrowEdgeViewFTA extends PolylineEdgeView { @@ -128,7 +128,7 @@ export class FTANodeView extends RectangularNodeView { @injectable() export class FTAGraphView extends SGraphView { - render(model: Readonly, context: RenderingContext): VNode { + render(model: Readonly, context: RenderingContext): VNode { if (model.children.length !== 0) { const topEvent = model.children.find(node => node instanceof FTANode && node.nodeType === FTNodeType.TOPEVENT); if (topEvent) { From 0f605de9d2f7365e47e8fe38de179b1d085ed9d1 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 6 Nov 2023 16:04:48 +0100 Subject: [PATCH 16/33] context menu in webview --- .../fta/diagram/fta-diagram-generator.ts | 17 +-- extension/src-language-server/pasta-model.ts | 24 ++++ extension/src-language-server/utils.ts | 12 +- .../context-menu/context-menu-provider.ts | 38 ++++++ .../context-menu/context-menu-services.ts | 116 ++++++++++++++++++ extension/src-webview/css/context-menu.css | 6 + extension/src-webview/css/diagram.css | 1 + extension/src-webview/di.config.ts | 9 +- extension/src-webview/pasta-model.ts | 33 +++++ 9 files changed, 241 insertions(+), 15 deletions(-) create mode 100644 extension/src-language-server/pasta-model.ts create mode 100644 extension/src-webview/context-menu/context-menu-provider.ts create mode 100644 extension/src-webview/context-menu/context-menu-services.ts create mode 100644 extension/src-webview/css/context-menu.css create mode 100644 extension/src-webview/pasta-model.ts 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 d71ff540..a242bc96 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -17,9 +17,10 @@ import { AstNode } from "langium"; import { GeneratorContext, IdCache, LangiumDiagramGenerator } from "langium-sprotty"; -import { SLabel, SModelElement, SModelRoot, SNode } from "sprotty-protocol"; -import { Component, Condition, Gate, ModelFTA, TopEvent, isComponent, isCondition, isKNGate } from "../../generated/ast"; +import { SModelElement, SModelRoot, SNode } from "sprotty-protocol"; +import { Component, Condition, Gate, ModelFTA, isComponent, isCondition, isKNGate } from "../../generated/ast"; import { getDescription } from "../../utils"; +import { topOfAnalysis } from "../analysis/fta-cutSet-calculator"; import { FtaServices } from "../fta-module"; import { namedFtaElement } from "../utils"; import { DescriptionNode, FTAEdge, FTAGraph, FTANode, FTAPort } from "./fta-interfaces"; @@ -35,7 +36,7 @@ import { } from "./fta-model"; import { FtaSynthesisOptions, noCutSet, spofsSet } from "./fta-synthesis-options"; import { getFTNodeType, getTargets } from "./utils"; -import { topOfAnalysis } from "../analysis/fta-cutSet-calculator"; +import { PASTA_LABEL_TYPE, PastaLabel } from "../../pasta-model"; export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected readonly options: FtaSynthesisOptions; @@ -368,10 +369,10 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { * @param idCache The ID cache of the FTA model. * @returns SLabel element representing {@code label}. */ - protected createNodeLabel(label: string, id: string, idCache: IdCache): SLabel[] { + protected createNodeLabel(label: string, id: string, idCache: IdCache): PastaLabel[] { return [ - { - type: "label", + { + type: PASTA_LABEL_TYPE, id: idCache.uniqueId(id + "_label"), text: label, }, @@ -385,9 +386,9 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { * @param idCache The ID cache of the FTA model. * @returns SLabel element representing {@code label}. */ - protected createEdgeLabel(label: string, id: string, idCache: IdCache): SLabel[] { + protected createEdgeLabel(label: string, id: string, idCache: IdCache): PastaLabel[] { return [ - { + { type: "label:xref", id: idCache.uniqueId(id + "_label"), text: label, diff --git a/extension/src-language-server/pasta-model.ts b/extension/src-language-server/pasta-model.ts new file mode 100644 index 00000000..06ab902b --- /dev/null +++ b/extension/src-language-server/pasta-model.ts @@ -0,0 +1,24 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 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 { SLabel } from 'sprotty-protocol'; + + +export const PASTA_LABEL_TYPE = "label:pasta"; + +export interface PastaLabel extends SLabel { +} \ No newline at end of file diff --git a/extension/src-language-server/utils.ts b/extension/src-language-server/utils.ts index 089928cd..9510b581 100644 --- a/extension/src-language-server/utils.ts +++ b/extension/src-language-server/utils.ts @@ -17,8 +17,8 @@ import { AstNode, LangiumSharedServices } from "langium"; import { IdCache, LangiumSprottySharedServices } from "langium-sprotty"; -import { SLabel } from "sprotty-protocol"; import { URI } from "vscode-uri"; +import { PastaLabel } from "./pasta-model"; import { labelManagementValue } from "./synthesis-options"; /** @@ -42,8 +42,8 @@ export function getDescription( labelWidth: number, nodeId: string, idCache: IdCache -): SLabel[] { - const labels: SLabel[] = []; +): PastaLabel[] { + const labels: PastaLabel[] = []; const words = description.split(" "); let current = ""; switch (labelManagement) { @@ -51,7 +51,7 @@ export function getDescription( break; case labelManagementValue.ORIGINAL: // show complete description in one line - labels.push({ + labels.push({ type: "label", id: idCache.uniqueId(nodeId + "_label"), text: description, @@ -64,7 +64,7 @@ export function getDescription( for (let i = 1; i < words.length && current.length + words[i].length <= labelWidth; i++) { current += " " + words[i]; } - labels.push({ + labels.push({ type: "label", id: idCache.uniqueId(nodeId + "_label"), text: current + "...", @@ -84,7 +84,7 @@ export function getDescription( } descriptions.push(current); for (let i = descriptions.length - 1; i >= 0; i--) { - labels.push({ + labels.push({ type: "label", id: idCache.uniqueId(nodeId + "_label"), text: descriptions[i], diff --git a/extension/src-webview/context-menu/context-menu-provider.ts b/extension/src-webview/context-menu/context-menu-provider.ts new file mode 100644 index 00000000..53d384cc --- /dev/null +++ b/extension/src-webview/context-menu/context-menu-provider.ts @@ -0,0 +1,38 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 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 { injectable } from "inversify"; +import { IContextMenuItemProvider, LabeledAction, SModelRoot } from "sprotty"; +import { Point } from "sprotty-protocol"; +import { FTA_GRAPH_TYPE } from "../fta/fta-model"; + +@injectable() +export class ContextMenuProvider implements IContextMenuItemProvider { + getItems(root: Readonly, lastMousePosition?: Point | undefined): Promise { + if (root.type === FTA_GRAPH_TYPE) { + return Promise.resolve([ + { + label: "Add Node", + actions: [] + } as LabeledAction + ]); + } else { + return Promise.resolve([]); + } + } + +} \ No newline at end of file diff --git a/extension/src-webview/context-menu/context-menu-services.ts b/extension/src-webview/context-menu/context-menu-services.ts new file mode 100644 index 00000000..fbb70e63 --- /dev/null +++ b/extension/src-webview/context-menu/context-menu-services.ts @@ -0,0 +1,116 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 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 { injectable } from "inversify"; +import { Anchor, IContextMenuService, MenuItem } from "sprotty"; + +@injectable() +export class ContextMenuService implements IContextMenuService { + protected contextmenuID = "contextMenu"; //ID used to find the contextmenu + + protected onHide: any; // if the contextmenu should be hidden and there was a hide method provided + + show(items: MenuItem[], anchor: Anchor, onHide?: (() => void) | undefined): void { + //If no menu exists we want to create one + let menu = document.getElementById(this.contextmenuID); + if (menu === null) { + // creates the context menu and styles it + menu = document.createElement("ul"); + menu.id = this.contextmenuID; + this.setupMenuEntrys(menu); + menu.style.marginTop = "-1px"; + menu.style.marginLeft = "-1px"; + menu.style.backgroundColor = "#f4f5f6"; + menu.style.border = "2px solid #bfc2c3"; + + // if the context menu is leaved, we hide it + menu.addEventListener("mouseleave", () => { + if (menu !== null) { + menu.style.display = "none"; + } + if (this.onHide !== undefined) { this.onHide();} + }); + + // adds the context menu to the dom + const sprotty = document.getElementsByClassName("sprotty"); + if (sprotty.length !== 0) { + sprotty[0].appendChild(menu); + } else { + return; + } + } + //if a contextmenu was opened before there may be items in it therefor we reset it here + menu.innerHTML = ""; + menu.style.backgroundColor = "#f4f5f6"; + + //for every structured change we can do we want to display it to the user + for (const item of items) { + //Create an item to add to the menu via dom manipulation + const new_item = document.createElement("li"); + this.setupItemEntrys(new_item); + new_item.innerText = item.label; //label is shown to the user + new_item.id = item.label; + + // simple mouselisteners so the color changes to indicate what is selected + new_item.addEventListener("mouseenter", (ev) => { + new_item.style.backgroundColor = "#bae5dd"; + new_item.style.border = "1px solid #40c2a8"; + new_item.style.borderRadius = "5px"; + }); + new_item.addEventListener("mouseleave", (ev) => { + new_item.style.backgroundColor = "#f4f5f6"; + new_item.style.border = ""; + new_item.style.borderRadius = ""; + }); + //actually appends the items to the context menu + menu.appendChild(new_item); + } + + //Displays the contextmenu + menu.style.display = "block"; + this.onHide = onHide; + + //Positioning of the context menu + menu.style.left = anchor.x.toString() + "px"; + menu.style.top = anchor.y.toString() + "px"; + + const window_height = menu.parentElement!.offsetHeight; + const window_width = menu.parentElement!.offsetWidth; + //if the contextmenu would be partially outside the view we need to relocate it so it fits inside + if (menu.offsetHeight + menu.offsetTop > window_height) + menu.style.top = (window_height - menu.offsetHeight).toString() + "px"; + + if (menu.offsetWidth + menu.offsetLeft > window_width) + menu.style.left = (window_width - menu.offsetWidth).toString() + "px"; + } + + setupItemEntrys(item: HTMLElement): void { + item.style.display = "block"; + item.style.backgroundColor = "#f4f5f6"; + item.style.position = "relative"; + item.style.padding = "5px"; + } + + setupMenuEntrys(menu: HTMLElement): void { + menu.style.float = "right"; + menu.style.position = "absolute"; + menu.style.listStyle = "none"; + menu.style.padding = "0"; + menu.style.display = "none"; + menu.style.color = "#3e4144"; + } +} diff --git a/extension/src-webview/css/context-menu.css b/extension/src-webview/css/context-menu.css new file mode 100644 index 00000000..17970cde --- /dev/null +++ b/extension/src-webview/css/context-menu.css @@ -0,0 +1,6 @@ +.context-menu { + margin-top: -1px; + margin-left: -1px; + background-color: #f4f5f6; + border: 2px solid #bfc2c3; +} \ No newline at end of file diff --git a/extension/src-webview/css/diagram.css b/extension/src-webview/css/diagram.css index ed2f200b..a7d40990 100644 --- a/extension/src-webview/css/diagram.css +++ b/extension/src-webview/css/diagram.css @@ -3,6 +3,7 @@ @import "./theme.css"; @import "./stpa-diagram.css"; @import "./fta-diagram.css"; +@import "./context-menu.css"; /* sprotty and black/white colors */ .vscode-high-contrast .print-node { diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index d1b40cfa..4d01031f 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -35,15 +35,18 @@ import { configureCommand, configureModelElement, loadDefaultModules, - overrideViewerOptions, + overrideViewerOptions } from "sprotty"; import { SvgCommand } from "./actions"; +import { ContextMenuService } from "./context-menu/context-menu-services"; +import { ContextMenuProvider } from "./context-menu/context-menu-provider"; 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 { PastaModelViewer } from "./model-viewer"; import { optionsModule } from "./options/options-module"; +import { PASTA_LABEL_TYPE, PastaLabel } from "./pasta-model"; import { sidebarModule } from "./sidebar"; import { CSEdge, @@ -84,6 +87,9 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = bind(SvgPostprocessor).toSelf().inSingletonScope(); bind(TYPES.HiddenVNodePostprocessor).toService(SvgPostprocessor); configureCommand({ bind, isBound }, SvgCommand); + // context-menu + bind(TYPES.IContextMenuService).to(ContextMenuService); + bind(TYPES.IContextMenuItemProvider).to(ContextMenuProvider); // configure the diagram elements const context = { bind, unbind, isBound, rebind }; @@ -92,6 +98,7 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = configureModelElement(context, "html", HtmlRoot, HtmlRootView); configureModelElement(context, "pre-rendered", PreRenderedElement, PreRenderedView); + configureModelElement(context, PASTA_LABEL_TYPE, PastaLabel, SLabelView); // STPA configureModelElement(context, "graph", SGraph, STPAGraphView); configureModelElement(context, DUMMY_NODE_TYPE, CSNode, CSNodeView); diff --git a/extension/src-webview/pasta-model.ts b/extension/src-webview/pasta-model.ts new file mode 100644 index 00000000..6629f891 --- /dev/null +++ b/extension/src-webview/pasta-model.ts @@ -0,0 +1,33 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 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 { + SLabel, + alignFeature, + boundsFeature, + edgeLayoutFeature, + fadeFeature, + layoutableChildFeature, + selectFeature +} from "sprotty"; + +export const PASTA_LABEL_TYPE = "label:pasta"; + +export class PastaLabel extends SLabel { + static readonly DEFAULT_FEATURES = [boundsFeature, alignFeature, layoutableChildFeature, + edgeLayoutFeature, fadeFeature, selectFeature]; +} \ No newline at end of file From e3633efa023ede09bb74fcac3bc9849f3e288ca3 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 6 Nov 2023 16:33:54 +0100 Subject: [PATCH 17/33] context menu: refactoring --- .../context-menu/context-menu-provider.ts | 2 +- .../context-menu/context-menu-services.ts | 140 +++++++++--------- extension/src-webview/css/context-menu.css | 18 +++ 3 files changed, 88 insertions(+), 72 deletions(-) diff --git a/extension/src-webview/context-menu/context-menu-provider.ts b/extension/src-webview/context-menu/context-menu-provider.ts index 53d384cc..7af57f60 100644 --- a/extension/src-webview/context-menu/context-menu-provider.ts +++ b/extension/src-webview/context-menu/context-menu-provider.ts @@ -26,7 +26,7 @@ export class ContextMenuProvider implements IContextMenuItemProvider { if (root.type === FTA_GRAPH_TYPE) { return Promise.resolve([ { - label: "Add Node", + label: "Cut Set Analysis", actions: [] } as LabeledAction ]); diff --git a/extension/src-webview/context-menu/context-menu-services.ts b/extension/src-webview/context-menu/context-menu-services.ts index fbb70e63..a8841f2a 100644 --- a/extension/src-webview/context-menu/context-menu-services.ts +++ b/extension/src-webview/context-menu/context-menu-services.ts @@ -20,29 +20,60 @@ import { Anchor, IContextMenuService, MenuItem } from "sprotty"; @injectable() export class ContextMenuService implements IContextMenuService { - protected contextmenuID = "contextMenu"; //ID used to find the contextmenu - - protected onHide: any; // if the contextmenu should be hidden and there was a hide method provided + /* The id of the context menu. */ + protected contextMenuID = "contextMenu"; show(items: MenuItem[], anchor: Anchor, onHide?: (() => void) | undefined): void { - //If no menu exists we want to create one - let menu = document.getElementById(this.contextmenuID); + // create or get the context menu + const menu = this.getOrCreateContextMenu(onHide); + // reset content of the menu + menu.innerHTML = ""; + + // add the items to the menu + for (const item of items) { + this.addItemToContextMenu(menu, item); + } + + // display the context menu + menu.style.display = "block"; + + // position the context menu + menu.style.left = anchor.x.toString() + "px"; + menu.style.top = anchor.y.toString() + "px"; + + const window_height = menu.parentElement!.offsetHeight; + const window_width = menu.parentElement!.offsetWidth; + // if the context menu would be partially outside the view, we relocate it so it fits inside + if (menu.offsetHeight + menu.offsetTop > window_height) { + menu.style.top = (window_height - menu.offsetHeight).toString() + "px"; + } + if (menu.offsetWidth + menu.offsetLeft > window_width) { + menu.style.left = (window_width - menu.offsetWidth).toString() + "px"; + } + } + + /** + * Creates a context menu with the "contextMenuID" if it does not exist yet, and adds it to the DOM. + * Otherwise it returns the existing one. + * @returns the context menu with the "contextMenuID". + */ + protected getOrCreateContextMenu(onHide?: () => void): HTMLElement { + let menu = document.getElementById(this.contextMenuID); if (menu === null) { - // creates the context menu and styles it + // creates the context menu menu = document.createElement("ul"); - menu.id = this.contextmenuID; - this.setupMenuEntrys(menu); - menu.style.marginTop = "-1px"; - menu.style.marginLeft = "-1px"; - menu.style.backgroundColor = "#f4f5f6"; - menu.style.border = "2px solid #bfc2c3"; + menu.id = this.contextMenuID; + menu.style.display = "none"; + menu.classList.add("context-menu"); - // if the context menu is leaved, we hide it + // if the context menu is left, we hide it menu.addEventListener("mouseleave", () => { if (menu !== null) { menu.style.display = "none"; } - if (this.onHide !== undefined) { this.onHide();} + if (onHide !== undefined) { + onHide(); + } }); // adds the context menu to the dom @@ -50,67 +81,34 @@ export class ContextMenuService implements IContextMenuService { if (sprotty.length !== 0) { sprotty[0].appendChild(menu); } else { - return; + console.log("Context menu could not be added to the DOM."); + return menu; } } - //if a contextmenu was opened before there may be items in it therefor we reset it here - menu.innerHTML = ""; - menu.style.backgroundColor = "#f4f5f6"; - - //for every structured change we can do we want to display it to the user - for (const item of items) { - //Create an item to add to the menu via dom manipulation - const new_item = document.createElement("li"); - this.setupItemEntrys(new_item); - new_item.innerText = item.label; //label is shown to the user - new_item.id = item.label; - - // simple mouselisteners so the color changes to indicate what is selected - new_item.addEventListener("mouseenter", (ev) => { - new_item.style.backgroundColor = "#bae5dd"; - new_item.style.border = "1px solid #40c2a8"; - new_item.style.borderRadius = "5px"; - }); - new_item.addEventListener("mouseleave", (ev) => { - new_item.style.backgroundColor = "#f4f5f6"; - new_item.style.border = ""; - new_item.style.borderRadius = ""; - }); - //actually appends the items to the context menu - menu.appendChild(new_item); - } - - //Displays the contextmenu - menu.style.display = "block"; - this.onHide = onHide; - - //Positioning of the context menu - menu.style.left = anchor.x.toString() + "px"; - menu.style.top = anchor.y.toString() + "px"; - - const window_height = menu.parentElement!.offsetHeight; - const window_width = menu.parentElement!.offsetWidth; - //if the contextmenu would be partially outside the view we need to relocate it so it fits inside - if (menu.offsetHeight + menu.offsetTop > window_height) - menu.style.top = (window_height - menu.offsetHeight).toString() + "px"; - - if (menu.offsetWidth + menu.offsetLeft > window_width) - menu.style.left = (window_width - menu.offsetWidth).toString() + "px"; + return menu; } - setupItemEntrys(item: HTMLElement): void { - item.style.display = "block"; - item.style.backgroundColor = "#f4f5f6"; - item.style.position = "relative"; - item.style.padding = "5px"; - } + /** + * Creates a DOM element for the given {@code item} and adds it to the given {@code menu}. + * @param menu The menu to which the item should be added. + * @param item The item to be added to the menu. + */ + protected addItemToContextMenu(menu: HTMLElement, item: MenuItem): void { + // creates the dom element for the item + const domItem = document.createElement("li"); + domItem.classList.add("context-menu-item"); + domItem.innerText = item.label; + domItem.id = item.label; + + // highlights the item when the mouse is over it + domItem.addEventListener("mouseenter", () => { + domItem.classList.add("selected"); + }); + domItem.addEventListener("mouseleave", () => { + domItem.classList.remove("selected"); + }); - setupMenuEntrys(menu: HTMLElement): void { - menu.style.float = "right"; - menu.style.position = "absolute"; - menu.style.listStyle = "none"; - menu.style.padding = "0"; - menu.style.display = "none"; - menu.style.color = "#3e4144"; + // append the item to the menu + menu.appendChild(domItem); } } diff --git a/extension/src-webview/css/context-menu.css b/extension/src-webview/css/context-menu.css index 17970cde..e8ee26d8 100644 --- a/extension/src-webview/css/context-menu.css +++ b/extension/src-webview/css/context-menu.css @@ -3,4 +3,22 @@ margin-left: -1px; background-color: #f4f5f6; border: 2px solid #bfc2c3; + float: right; + position: absolute; + list-style: none; + padding: 0; + color: #3e4144; +} + +.context-menu-item { + display: block; + background-color: #f4f5f6; + position: relative; + padding: 5px; +} + +.selected { + background-color: #bae5dd; + border: 1px solid #40c2a8; + border-radius: 5px; } \ No newline at end of file From 465d261361aa9c160d2ef0e0f52bc3e2f81d6ee4 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 6 Nov 2023 16:58:43 +0100 Subject: [PATCH 18/33] context-menu: refactoring --- .../context-menu/context-menu-services.ts | 16 +++++++--------- extension/src-webview/css/context-menu.css | 6 +++++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/extension/src-webview/context-menu/context-menu-services.ts b/extension/src-webview/context-menu/context-menu-services.ts index a8841f2a..2fdf9495 100644 --- a/extension/src-webview/context-menu/context-menu-services.ts +++ b/extension/src-webview/context-menu/context-menu-services.ts @@ -26,17 +26,12 @@ export class ContextMenuService implements IContextMenuService { show(items: MenuItem[], anchor: Anchor, onHide?: (() => void) | undefined): void { // create or get the context menu const menu = this.getOrCreateContextMenu(onHide); - // reset content of the menu - menu.innerHTML = ""; // add the items to the menu for (const item of items) { this.addItemToContextMenu(menu, item); } - // display the context menu - menu.style.display = "block"; - // position the context menu menu.style.left = anchor.x.toString() + "px"; menu.style.top = anchor.y.toString() + "px"; @@ -63,17 +58,16 @@ export class ContextMenuService implements IContextMenuService { // creates the context menu menu = document.createElement("ul"); menu.id = this.contextMenuID; - menu.style.display = "none"; menu.classList.add("context-menu"); // if the context menu is left, we hide it menu.addEventListener("mouseleave", () => { - if (menu !== null) { - menu.style.display = "none"; - } if (onHide !== undefined) { onHide(); } + if (menu !== null) { + menu.classList.add("hidden"); + } }); // adds the context menu to the dom @@ -84,6 +78,10 @@ export class ContextMenuService implements IContextMenuService { console.log("Context menu could not be added to the DOM."); return menu; } + } else { + // reset the menu + menu.innerHTML = ""; + menu.classList.remove("hidden"); } return menu; } diff --git a/extension/src-webview/css/context-menu.css b/extension/src-webview/css/context-menu.css index e8ee26d8..2c1b3851 100644 --- a/extension/src-webview/css/context-menu.css +++ b/extension/src-webview/css/context-menu.css @@ -10,6 +10,10 @@ color: #3e4144; } +.context-menu.hidden { + display: none; +} + .context-menu-item { display: block; background-color: #f4f5f6; @@ -17,7 +21,7 @@ padding: 5px; } -.selected { +.context-menu-item.selected { background-color: #bae5dd; border: 1px solid #40c2a8; border-radius: 5px; From 3e602a3802214a3099be66989190dc8f3133a2a4 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 7 Nov 2023 08:49:54 +0100 Subject: [PATCH 19/33] adjjusted layout config --- .../src-language-server/fta/diagram/fta-layout-config.ts | 5 +++-- extension/src-language-server/stpa/diagram/layout-config.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-layout-config.ts b/extension/src-language-server/fta/diagram/fta-layout-config.ts index 55be63e6..cfcd3a8f 100644 --- a/extension/src-language-server/fta/diagram/fta-layout-config.ts +++ b/extension/src-language-server/fta/diagram/fta-layout-config.ts @@ -25,14 +25,15 @@ export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { protected graphOptions(sgraph: FTAGraph, _index: SModelIndex): LayoutOptions { const options: LayoutOptions = { "org.eclipse.elk.direction": "DOWN", - "org.eclipse.elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", + "org.eclipse.elk.layered.nodePlacement.strategy": "BRANDES_KOEPF", "org.eclipse.elk.portConstraints": "FIXED_SIDE", "org.eclipse.elk.hierarchyHandling": "INCLUDE_CHILDREN", "org.eclipse.elk.spacing.portPort": "0.0", + "org.eclipse.elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED" }; if (sgraph.modelOrder) { - options["org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeOrdering"] = "NODES_AND_EDGES"; + options["org.eclipse.elk.layered.considerModelOrder.strategy"] = "NODES_AND_EDGES"; options["org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder"] = "true"; options["org.eclipse.elk.separateConnectedComponents"] = "false"; } diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index f0d61bb3..7717d970 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -65,7 +65,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { // model order is used to determine the order of the children if (snode.modelOrder) { - options['org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeOrdering'] = 'NODES_AND_EDGES'; + options['org.eclipse.elk.layered.considerModelOrder.strategy'] = 'NODES_AND_EDGES'; options['org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder'] = 'true'; options['org.eclipse.elk.separateConnectedComponents'] = 'false'; } @@ -114,7 +114,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { // model order is used to determine the order of the children if (node.modelOrder) { - options['org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeOrdering'] = 'NODES_AND_EDGES'; + options['org.eclipse.elk.layered.considerModelOrder.strategy'] = 'NODES_AND_EDGES'; options['org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder'] = 'true'; options['org.eclipse.elk.separateConnectedComponents'] = 'false'; } From 3082bb9e1f7762b6252a1a90ce6a1b03098af89c Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 7 Nov 2023 09:10:50 +0100 Subject: [PATCH 20/33] context menu: adjusted colors --- extension/src-webview/css/context-menu.css | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/extension/src-webview/css/context-menu.css b/extension/src-webview/css/context-menu.css index 2c1b3851..8b25fbee 100644 --- a/extension/src-webview/css/context-menu.css +++ b/extension/src-webview/css/context-menu.css @@ -1,13 +1,14 @@ .context-menu { margin-top: -1px; margin-left: -1px; - background-color: #f4f5f6; - border: 2px solid #bfc2c3; + background-color: var(--vscode-menu-background); + border: 1px solid var(--vscode-menu-border); + border-radius: 5px; float: right; position: absolute; list-style: none; - padding: 0; - color: #3e4144; + padding: 2px; + color: var(--vscode-menu-foreground); } .context-menu.hidden { @@ -16,13 +17,15 @@ .context-menu-item { display: block; - background-color: #f4f5f6; position: relative; padding: 5px; } .context-menu-item.selected { - background-color: #bae5dd; - border: 1px solid #40c2a8; + background-color: var(--vscode-menu-selectionBackground); + border: 1px solid ; border-radius: 5px; -} \ No newline at end of file + color: var(--vscode-menu-selectionForeground); +} + +/* --vscode-menu-separatorBackground: #d4d4d4; */ \ No newline at end of file From e8190e59aae33db27458cb4b5e0e488bfc9be413 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 7 Nov 2023 10:57:17 +0100 Subject: [PATCH 21/33] adjusted cursor visualization --- extension/src-webview/css/context-menu.css | 1 + extension/src-webview/css/diagram.css | 2 ++ extension/src-webview/css/fta-diagram.css | 1 + 3 files changed, 4 insertions(+) diff --git a/extension/src-webview/css/context-menu.css b/extension/src-webview/css/context-menu.css index 8b25fbee..10872828 100644 --- a/extension/src-webview/css/context-menu.css +++ b/extension/src-webview/css/context-menu.css @@ -19,6 +19,7 @@ display: block; position: relative; padding: 5px; + cursor: pointer; } .context-menu-item.selected { diff --git a/extension/src-webview/css/diagram.css b/extension/src-webview/css/diagram.css index a7d40990..d1b36d33 100644 --- a/extension/src-webview/css/diagram.css +++ b/extension/src-webview/css/diagram.css @@ -65,12 +65,14 @@ body[class='vscode-light'] .sprotty-label { fill: black; stroke-width: 0; + cursor: default; } .sprotty-label { fill: var(--vscode-editorActiveLineNumber-foreground); stroke-width: 0; font-family: Arial, sans-serif; + cursor: default; } .sprotty-button { diff --git a/extension/src-webview/css/fta-diagram.css b/extension/src-webview/css/fta-diagram.css index 5ae8c894..a94d0ee8 100644 --- a/extension/src-webview/css/fta-diagram.css +++ b/extension/src-webview/css/fta-diagram.css @@ -41,6 +41,7 @@ fill: dimgrey; stroke-width: 0; font-size: 10px; + cursor: default; } .fta-highlight-node{ From bd4abe8d127f30057a67b0d707734b54f84207c4 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 7 Nov 2023 17:12:28 +0100 Subject: [PATCH 22/33] adjusted stpa label id --- extension/src-language-server/stpa/diagram/diagram-generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index a113b46d..b470b51c 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -921,7 +921,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // show the name in the top line children.push({ type: "label", - id: idCache.uniqueId(nodeId + ".label"), + id: idCache.uniqueId(nodeId + "_label"), text: nodeName, }); return children; From 17da973c87bc1f9d9d55d642bd654468a70abe1d Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 8 Nov 2023 14:56:15 +0100 Subject: [PATCH 23/33] action in context menu works --- .../fta/fta-message-handler.ts | 16 ++++---- .../src-language-server/stpa/stpa-module.ts | 3 +- extension/src-webview/actions.ts | 17 ++++++++- .../context-menu/context-menu-provider.ts | 11 +++++- .../context-menu/context-menu-services.ts | 14 ++++++- extension/src-webview/css/context-menu.css | 4 +- .../src-webview/stpa/stpa-mouselistener.ts | 14 +++++-- extension/src/actions.ts | 15 ++++++++ extension/src/extension.ts | 4 +- extension/src/wview.ts | 37 ++++++++++++++----- 10 files changed, 106 insertions(+), 29 deletions(-) diff --git a/extension/src-language-server/fta/fta-message-handler.ts b/extension/src-language-server/fta/fta-message-handler.ts index 646edff4..6fe48414 100644 --- a/extension/src-language-server/fta/fta-message-handler.ts +++ b/extension/src-language-server/fta/fta-message-handler.ts @@ -22,7 +22,7 @@ import { ModelFTA } from "../generated/ast"; import { getModel } from "../utils"; import { determineCutSetsForFT, determineMinimalCutSets } from "./analysis/fta-cutSet-calculator"; import { FtaServices } from "./fta-module"; -import { cutSetsToString } from "./utils"; +import { cutSetsToString, namedFtaElement } from "./utils"; /** * Adds handlers for notifications regarding fta. @@ -49,8 +49,8 @@ function addCutSetsHandler( ftaServices: FtaServices, sharedServices: LangiumSprottySharedServices ): void { - connection.onRequest("cutSets/generate", async (uri: string) => { - return cutSetsRequested(uri, ftaServices, sharedServices, false); + connection.onRequest("cutSets/generate", async (content: {uri: string, startId?: string}) => { + return cutSetsRequested(content.uri, ftaServices, sharedServices, false, content.startId); }); connection.onRequest("cutSets/generateMinimal", async (uri: string) => { return cutSetsRequested(uri, ftaServices, sharedServices, true); @@ -72,14 +72,16 @@ async function cutSetsRequested( uri: string, ftaServices: FtaServices, sharedServices: LangiumSprottySharedServices, - minimal: boolean + minimal: boolean, + startId?: string ): Promise { const model = (await getModel(uri, sharedServices)) as ModelFTA; - const nodes: AstNode[] = [...model.components, ...model.conditions, ...model.gates]; + const nodes: namedFtaElement[] = [...model.components, ...model.conditions, ...model.gates]; if (model.topEvent) { nodes.push(model.topEvent); } - const cutSets = minimal ? determineMinimalCutSets(nodes) : determineCutSetsForFT(nodes); + const startNode = startId ? nodes.find((node) => node.name === startId) : undefined; + const cutSets = minimal ? determineMinimalCutSets(nodes) : determineCutSetsForFT(nodes, startNode); // determine single points of failure const spofs: string[] = []; for (const cutSet of cutSets) { @@ -100,4 +102,4 @@ async function cutSetsRequested( function resetCutSets(ftaServices: FtaServices): void { ftaServices.options.SynthesisOptions.resetCutSets(); return; -} \ No newline at end of file +} diff --git a/extension/src-language-server/stpa/stpa-module.ts b/extension/src-language-server/stpa/stpa-module.ts index 7150f6bb..84547296 100644 --- a/extension/src-language-server/stpa/stpa-module.ts +++ b/extension/src-language-server/stpa/stpa-module.ts @@ -38,6 +38,7 @@ import { StpaLayoutConfigurator } from "./diagram/layout-config"; import { StpaSynthesisOptions } from "./diagram/stpa-synthesis-options"; import { StpaScopeProvider } from "./stpa-scopeProvider"; import { StpaValidationRegistry, StpaValidator } from "./stpa-validator"; +import { LayoutEngine } from "../layout-engine"; /** * Declaration of custom services - add your own service classes here. @@ -80,7 +81,7 @@ export const STPAModule: Module new StpaDiagramGenerator(services), ModelLayoutEngine: (services) => - new ElkLayoutEngine( + new LayoutEngine( services.layout.ElkFactory, services.layout.ElementFilter, services.layout.LayoutConfigurator diff --git a/extension/src-webview/actions.ts b/extension/src-webview/actions.ts index 747d5306..f800a229 100644 --- a/extension/src-webview/actions.ts +++ b/extension/src-webview/actions.ts @@ -17,7 +17,7 @@ import { inject } from "inversify"; import { CommandExecutionContext, CommandResult, HiddenCommand, TYPES, isExportable, isHoverable, isSelectable, isViewport } from "sprotty"; -import { RequestAction, ResponseAction, generateRequestId } from "sprotty-protocol"; +import { RequestAction, ResponseAction, generateRequestId, Action } from "sprotty-protocol"; /** Requests the current SVG from the client. */ @@ -91,4 +91,19 @@ export class SvgCommand extends HiddenCommand { modelChanged: false }; } +} + +export interface CutSetAnalysisAction extends Action { + kind: typeof CutSetAnalysisAction.KIND; + startId: string +} +export namespace CutSetAnalysisAction { + export const KIND = 'cutSetAnalysis'; + + export function create(startId: string,): CutSetAnalysisAction { + return { + kind: KIND, + startId, + }; + } } \ No newline at end of file diff --git a/extension/src-webview/context-menu/context-menu-provider.ts b/extension/src-webview/context-menu/context-menu-provider.ts index 7af57f60..9da4e833 100644 --- a/extension/src-webview/context-menu/context-menu-provider.ts +++ b/extension/src-webview/context-menu/context-menu-provider.ts @@ -18,16 +18,23 @@ import { injectable } from "inversify"; import { IContextMenuItemProvider, LabeledAction, SModelRoot } from "sprotty"; import { Point } from "sprotty-protocol"; -import { FTA_GRAPH_TYPE } from "../fta/fta-model"; +import { FTANode, FTA_GRAPH_TYPE, FTA_NODE_TYPE } from "../fta/fta-model"; +import { CutSetAnalysisAction } from "../actions"; @injectable() export class ContextMenuProvider implements IContextMenuItemProvider { getItems(root: Readonly, lastMousePosition?: Point | undefined): Promise { if (root.type === FTA_GRAPH_TYPE) { + // find node that was clicked on + const clickedNode = root.children.find(child => { + if (child.type === FTA_NODE_TYPE) { + return (child as FTANode).selected; + } + }); return Promise.resolve([ { label: "Cut Set Analysis", - actions: [] + actions: [{ kind: "cutSetAnalysis", startId: clickedNode?.id} as CutSetAnalysisAction] } as LabeledAction ]); } else { diff --git a/extension/src-webview/context-menu/context-menu-services.ts b/extension/src-webview/context-menu/context-menu-services.ts index 2fdf9495..ebf7a5f7 100644 --- a/extension/src-webview/context-menu/context-menu-services.ts +++ b/extension/src-webview/context-menu/context-menu-services.ts @@ -15,11 +15,16 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { injectable } from "inversify"; +import { injectable, inject } from "inversify"; import { Anchor, IContextMenuService, MenuItem } from "sprotty"; +import { ActionNotification } from "sprotty-vscode-protocol"; +import { VsCodeMessenger } from "sprotty-vscode-webview/lib/services"; +import { HOST_EXTENSION } from "vscode-messenger-common"; +import { Messenger } from "vscode-messenger-webview"; @injectable() export class ContextMenuService implements IContextMenuService { + @inject(VsCodeMessenger) protected messenger: Messenger; /* The id of the context menu. */ protected contextMenuID = "contextMenu"; @@ -106,6 +111,13 @@ export class ContextMenuService implements IContextMenuService { domItem.classList.remove("selected"); }); + // executes the action when the item is clicked + domItem.addEventListener("click", () => { + for (const action of item.actions ?? []) { + this.messenger.sendNotification(ActionNotification, HOST_EXTENSION, { clientId: "", action: action }); + } + }); + // append the item to the menu menu.appendChild(domItem); } diff --git a/extension/src-webview/css/context-menu.css b/extension/src-webview/css/context-menu.css index 10872828..0b092fbb 100644 --- a/extension/src-webview/css/context-menu.css +++ b/extension/src-webview/css/context-menu.css @@ -7,7 +7,7 @@ float: right; position: absolute; list-style: none; - padding: 2px; + padding: 4px; color: var(--vscode-menu-foreground); } @@ -24,7 +24,7 @@ .context-menu-item.selected { background-color: var(--vscode-menu-selectionBackground); - border: 1px solid ; + /* border: 1px solid ; */ border-radius: 5px; color: var(--vscode-menu-selectionForeground); } diff --git a/extension/src-webview/stpa/stpa-mouselistener.ts b/extension/src-webview/stpa/stpa-mouselistener.ts index 2554a36d..c01c3ec7 100644 --- a/extension/src-webview/stpa/stpa-mouselistener.ts +++ b/extension/src-webview/stpa/stpa-mouselistener.ts @@ -1,15 +1,22 @@ -import { MouseListener, SLabel, SModelElement } from "sprotty"; +import { MouseListener, SLabel, SNode, SModelElement } from "sprotty"; import { Action } from "sprotty-protocol"; import { flagConnectedElements, flagSameAspect } from "./helper-methods"; import { STPAEdge, STPANode, STPA_NODE_TYPE } from "./stpa-model"; export class StpaMouseListener extends MouseListener { - protected flaggedElements: (STPANode | STPAEdge)[] = []; + protected lastSelected: SNode | undefined = undefined; mouseDown(target: SModelElement, event: MouseEvent): (Action | Promise)[] { // when a label is selected, we are interested in its parent node target = target instanceof SLabel ? target.parent : target; + if (this.lastSelected) { + this.lastSelected.selected = false; + } + if (target instanceof SNode) { + target.selected = true; + this.lastSelected = target; + } if (target.type === STPA_NODE_TYPE) { if (event.ctrlKey) { // when ctrl is pressed all nodes with the same aspect as the selected one should be highlighted @@ -34,5 +41,4 @@ export class StpaMouseListener extends MouseListener { } this.flaggedElements = []; } - -} \ No newline at end of file +} diff --git a/extension/src/actions.ts b/extension/src/actions.ts index 11710c6d..8ab8621c 100644 --- a/extension/src/actions.ts +++ b/extension/src/actions.ts @@ -59,4 +59,19 @@ export namespace GenerateSVGsAction { export function isThisAction(action: Action): action is GenerateSVGsAction { return action.kind === GenerateSVGsAction.KIND; } +} + +export interface CutSetAnalysisAction extends Action { + kind: typeof CutSetAnalysisAction.KIND; + startId: string +} +export namespace CutSetAnalysisAction { + export const KIND = 'cutSetAnalysis'; + + export function create(startId: string,): CutSetAnalysisAction { + return { + kind: KIND, + startId, + }; + } } \ No newline at end of file diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 843da6f8..8030666b 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -212,8 +212,8 @@ function registerSTPACommands(manager: StpaLspVscodeExtension, context: vscode.E function registerFTACommands(manager: StpaLspVscodeExtension, context: vscode.ExtensionContext, options: { extensionPrefix: string; }): void { // commands for computing and displaying the (minimal) cut sets of the fault tree. context.subscriptions.push( - vscode.commands.registerCommand(options.extensionPrefix + ".fta.cutSets", async (uri: vscode.Uri) => { - const cutSets: string[] = await languageClient.sendRequest("cutSets/generate", uri.path); + vscode.commands.registerCommand(options.extensionPrefix + ".fta.cutSets", async (uri: vscode.Uri, startId?: string) => { + const cutSets: string[] = await languageClient.sendRequest("cutSets/generate", {uri: uri.path, startId}); await manager.openDiagram(uri); handleCutSets(manager, cutSets, false); }) diff --git a/extension/src/wview.ts b/extension/src/wview.ts index 57b54b22..39446c4d 100644 --- a/extension/src/wview.ts +++ b/extension/src/wview.ts @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2022 by + * Copyright 2022-2023 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -18,7 +18,7 @@ import { isActionMessage, SelectAction } from "sprotty-protocol"; import { LspWebviewEndpoint } from "sprotty-vscode/lib/lsp"; import * as vscode from "vscode"; -import { SendConfigAction } from "./actions"; +import { CutSetAnalysisAction, SendConfigAction } from "./actions"; export class StpaLspWebview extends LspWebviewEndpoint { receiveAction(message: any): Promise { @@ -32,6 +32,9 @@ export class StpaLspWebview extends LspWebviewEndpoint { case SelectAction.KIND: this.handleSelectAction(message.action as SelectAction); break; + case CutSetAnalysisAction.KIND: + this.handleCutSetAnalysisAction(message.action as CutSetAnalysisAction); + break; } } return super.receiveAction(message); @@ -42,13 +45,9 @@ export class StpaLspWebview extends LspWebviewEndpoint { * @param action The SelectAction. */ protected handleSelectAction(action: SelectAction): void { - // the uri string must be desrialized first, since sprotty serializes it and langium does not - if (this.diagramIdentifier) { - let uriString = this.diagramIdentifier.uri.toString(); - const match = uriString.match(/file:\/\/\/([a-z]):/i); - if (match) { - uriString = "file:///" + match[1] + "%3A" + uriString.substring(match[0].length); - } + // the uri string must be deserialized first, since sprotty serializes it and langium does not + const uriString = this.deserializeUriOfDiagramIdentifier(); + if (uriString !== "") { // send ID of the first selected element to the language server to highlight the textual definition in the editor this.languageClient.sendNotification("diagram/selected", { label: action.selectedElementsIDs[0], @@ -73,4 +72,24 @@ export class StpaLspWebview extends LspWebviewEndpoint { const configOptions = vscode.workspace.getConfiguration("pasta"); action.options.forEach((element) => configOptions.update(element.id, element.value)); } + + protected handleCutSetAnalysisAction(action: CutSetAnalysisAction): void { + const uriString = this.deserializeUriOfDiagramIdentifier(); + if (uriString !== "") { + const uri = vscode.Uri.parse(uriString); + vscode.commands.executeCommand("pasta.fta.cutSets", uri, action.startId); + } + } + + protected deserializeUriOfDiagramIdentifier(): string { + if (this.diagramIdentifier) { + let uriString = this.diagramIdentifier.uri.toString(); + const match = uriString.match(/file:\/\/\/([a-z]):/i); + if (match) { + uriString = "file:///" + match[1] + "%3A" + uriString.substring(match[0].length); + } + return uriString; + } + return ""; + } } From f4b4ac64ee02cc4d6a8cbc399fbabc937875f96b Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 8 Nov 2023 15:55:25 +0100 Subject: [PATCH 24/33] fixed clicking on label --- .../fta/diagram/fta-diagram-generator.ts | 13 +++--- extension/src-language-server/utils.ts | 12 +++--- .../context-menu-mouse-listener.ts} | 12 ++++-- .../context-menu/context-menu-provider.ts | 6 ++- .../src-webview/context-menu/di.config.ts | 40 +++++++++++++++++++ extension/src-webview/di.config.ts | 10 ++--- extension/src-webview/pasta-model.ts | 33 --------------- .../src-webview/stpa/stpa-mouselistener.ts | 14 ++----- 8 files changed, 75 insertions(+), 65 deletions(-) rename extension/{src-language-server/pasta-model.ts => src-webview/context-menu/context-menu-mouse-listener.ts} (53%) create mode 100644 extension/src-webview/context-menu/di.config.ts delete mode 100644 extension/src-webview/pasta-model.ts 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 a242bc96..d891f30c 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -17,7 +17,7 @@ import { AstNode } from "langium"; import { GeneratorContext, IdCache, LangiumDiagramGenerator } from "langium-sprotty"; -import { SModelElement, SModelRoot, SNode } from "sprotty-protocol"; +import { SModelElement, SModelRoot, SNode, SLabel } from "sprotty-protocol"; import { Component, Condition, Gate, ModelFTA, isComponent, isCondition, isKNGate } from "../../generated/ast"; import { getDescription } from "../../utils"; import { topOfAnalysis } from "../analysis/fta-cutSet-calculator"; @@ -36,7 +36,6 @@ import { } from "./fta-model"; import { FtaSynthesisOptions, noCutSet, spofsSet } from "./fta-synthesis-options"; import { getFTNodeType, getTargets } from "./utils"; -import { PASTA_LABEL_TYPE, PastaLabel } from "../../pasta-model"; export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected readonly options: FtaSynthesisOptions; @@ -369,10 +368,10 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { * @param idCache The ID cache of the FTA model. * @returns SLabel element representing {@code label}. */ - protected createNodeLabel(label: string, id: string, idCache: IdCache): PastaLabel[] { + protected createNodeLabel(label: string, id: string, idCache: IdCache): SLabel[] { return [ - { - type: PASTA_LABEL_TYPE, + { + type: 'label', id: idCache.uniqueId(id + "_label"), text: label, }, @@ -386,9 +385,9 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { * @param idCache The ID cache of the FTA model. * @returns SLabel element representing {@code label}. */ - protected createEdgeLabel(label: string, id: string, idCache: IdCache): PastaLabel[] { + protected createEdgeLabel(label: string, id: string, idCache: IdCache): SLabel[] { return [ - { + { type: "label:xref", id: idCache.uniqueId(id + "_label"), text: label, diff --git a/extension/src-language-server/utils.ts b/extension/src-language-server/utils.ts index 9510b581..78028a2a 100644 --- a/extension/src-language-server/utils.ts +++ b/extension/src-language-server/utils.ts @@ -18,8 +18,8 @@ import { AstNode, LangiumSharedServices } from "langium"; import { IdCache, LangiumSprottySharedServices } from "langium-sprotty"; import { URI } from "vscode-uri"; -import { PastaLabel } from "./pasta-model"; import { labelManagementValue } from "./synthesis-options"; +import { SLabel } from 'sprotty-protocol'; /** * Determines the model for {@code uri}. @@ -42,8 +42,8 @@ export function getDescription( labelWidth: number, nodeId: string, idCache: IdCache -): PastaLabel[] { - const labels: PastaLabel[] = []; +): SLabel[] { + const labels: SLabel[] = []; const words = description.split(" "); let current = ""; switch (labelManagement) { @@ -51,7 +51,7 @@ export function getDescription( break; case labelManagementValue.ORIGINAL: // show complete description in one line - labels.push({ + labels.push({ type: "label", id: idCache.uniqueId(nodeId + "_label"), text: description, @@ -64,7 +64,7 @@ export function getDescription( for (let i = 1; i < words.length && current.length + words[i].length <= labelWidth; i++) { current += " " + words[i]; } - labels.push({ + labels.push({ type: "label", id: idCache.uniqueId(nodeId + "_label"), text: current + "...", @@ -84,7 +84,7 @@ export function getDescription( } descriptions.push(current); for (let i = descriptions.length - 1; i >= 0; i--) { - labels.push({ + labels.push({ type: "label", id: idCache.uniqueId(nodeId + "_label"), text: descriptions[i], diff --git a/extension/src-language-server/pasta-model.ts b/extension/src-webview/context-menu/context-menu-mouse-listener.ts similarity index 53% rename from extension/src-language-server/pasta-model.ts rename to extension/src-webview/context-menu/context-menu-mouse-listener.ts index 06ab902b..5177a247 100644 --- a/extension/src-language-server/pasta-model.ts +++ b/extension/src-webview/context-menu/context-menu-mouse-listener.ts @@ -15,10 +15,16 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { SLabel } from 'sprotty-protocol'; +import { ContextMenuMouseListener, SModelElement, SLabel } from "sprotty"; -export const PASTA_LABEL_TYPE = "label:pasta"; +export class PastaContextMenuMouseListener extends ContextMenuMouseListener { -export interface PastaLabel extends SLabel { + protected async showContextMenu(target: SModelElement, event: MouseEvent): Promise { + if (target instanceof SLabel) { + super.showContextMenu(target.parent, event); + } else { + super.showContextMenu(target, event); + } + } } \ No newline at end of file diff --git a/extension/src-webview/context-menu/context-menu-provider.ts b/extension/src-webview/context-menu/context-menu-provider.ts index 9da4e833..71240c0d 100644 --- a/extension/src-webview/context-menu/context-menu-provider.ts +++ b/extension/src-webview/context-menu/context-menu-provider.ts @@ -35,7 +35,11 @@ export class ContextMenuProvider implements IContextMenuItemProvider { { label: "Cut Set Analysis", actions: [{ kind: "cutSetAnalysis", startId: clickedNode?.id} as CutSetAnalysisAction] - } as LabeledAction + } as LabeledAction, + // { + // label: "Minimal Cut Set Analysis", + // actions: [{ kind: "minimalCutSetAnalysis", startId: clickedNode?.id} as MinimalCutSetAnalysisAction] + // } as LabeledAction ]); } else { return Promise.resolve([]); diff --git a/extension/src-webview/context-menu/di.config.ts b/extension/src-webview/context-menu/di.config.ts new file mode 100644 index 00000000..68ae2635 --- /dev/null +++ b/extension/src-webview/context-menu/di.config.ts @@ -0,0 +1,40 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 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 { ContainerModule } from "inversify"; +import { ContextMenuProviderRegistry, IContextMenuService, TYPES } from "sprotty"; +import { PastaContextMenuMouseListener } from "./context-menu-mouse-listener"; + +const pastaContextMenuModule = new ContainerModule(bind => { + bind(TYPES.IContextMenuServiceProvider).toProvider(ctx => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + return () => { + return new Promise((resolve, reject) => { + if (ctx.container.isBound(TYPES.IContextMenuService)) { + resolve(ctx.container.get(TYPES.IContextMenuService)); + } else { + reject(); + } + }); + }; + }); + bind(PastaContextMenuMouseListener).toSelf().inSingletonScope(); + bind(TYPES.MouseListener).toService(PastaContextMenuMouseListener); + bind(TYPES.IContextMenuProviderRegistry).to(ContextMenuProviderRegistry); +}); + +export default pastaContextMenuModule; diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index 4d01031f..1471e0ea 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -35,7 +35,8 @@ import { configureCommand, configureModelElement, loadDefaultModules, - overrideViewerOptions + overrideViewerOptions, + contextMenuModule } from "sprotty"; import { SvgCommand } from "./actions"; import { ContextMenuService } from "./context-menu/context-menu-services"; @@ -46,7 +47,6 @@ import { DescriptionNode, FTAEdge, FTAGraph, FTANode, FTAPort, FTA_DESCRIPTION_N import { DescriptionNodeView, FTAGraphView, FTAInvisibleEdgeView, FTANodeView, PolylineArrowEdgeViewFTA } from "./fta/fta-views"; import { PastaModelViewer } from "./model-viewer"; import { optionsModule } from "./options/options-module"; -import { PASTA_LABEL_TYPE, PastaLabel } from "./pasta-model"; import { sidebarModule } from "./sidebar"; import { CSEdge, @@ -72,6 +72,7 @@ import { STPAGraphView, STPANodeView, } from "./stpa/stpa-views"; +import pastaContextMenuModule from "./context-menu/di.config"; const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); @@ -98,7 +99,6 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = configureModelElement(context, "html", HtmlRoot, HtmlRootView); configureModelElement(context, "pre-rendered", PreRenderedElement, PreRenderedView); - configureModelElement(context, PASTA_LABEL_TYPE, PastaLabel, SLabelView); // STPA configureModelElement(context, "graph", SGraph, STPAGraphView); configureModelElement(context, DUMMY_NODE_TYPE, CSNode, CSNodeView); @@ -121,8 +121,8 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = export function createPastaDiagramContainer(widgetId: string): Container { const container = new Container(); - loadDefaultModules(container); - container.load(pastaDiagramModule, sidebarModule, optionsModule); + loadDefaultModules(container, {exclude: [contextMenuModule]}); + container.load(pastaContextMenuModule, pastaDiagramModule, sidebarModule, optionsModule); overrideViewerOptions(container, { needsClientLayout: true, needsServerLayout: true, diff --git a/extension/src-webview/pasta-model.ts b/extension/src-webview/pasta-model.ts deleted file mode 100644 index 6629f891..00000000 --- a/extension/src-webview/pasta-model.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient - * - * http://rtsys.informatik.uni-kiel.de/kieler - * - * Copyright 2023 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 { - SLabel, - alignFeature, - boundsFeature, - edgeLayoutFeature, - fadeFeature, - layoutableChildFeature, - selectFeature -} from "sprotty"; - -export const PASTA_LABEL_TYPE = "label:pasta"; - -export class PastaLabel extends SLabel { - static readonly DEFAULT_FEATURES = [boundsFeature, alignFeature, layoutableChildFeature, - edgeLayoutFeature, fadeFeature, selectFeature]; -} \ No newline at end of file diff --git a/extension/src-webview/stpa/stpa-mouselistener.ts b/extension/src-webview/stpa/stpa-mouselistener.ts index c01c3ec7..2554a36d 100644 --- a/extension/src-webview/stpa/stpa-mouselistener.ts +++ b/extension/src-webview/stpa/stpa-mouselistener.ts @@ -1,22 +1,15 @@ -import { MouseListener, SLabel, SNode, SModelElement } from "sprotty"; +import { MouseListener, SLabel, SModelElement } from "sprotty"; import { Action } from "sprotty-protocol"; import { flagConnectedElements, flagSameAspect } from "./helper-methods"; import { STPAEdge, STPANode, STPA_NODE_TYPE } from "./stpa-model"; export class StpaMouseListener extends MouseListener { + protected flaggedElements: (STPANode | STPAEdge)[] = []; - protected lastSelected: SNode | undefined = undefined; mouseDown(target: SModelElement, event: MouseEvent): (Action | Promise)[] { // when a label is selected, we are interested in its parent node target = target instanceof SLabel ? target.parent : target; - if (this.lastSelected) { - this.lastSelected.selected = false; - } - if (target instanceof SNode) { - target.selected = true; - this.lastSelected = target; - } if (target.type === STPA_NODE_TYPE) { if (event.ctrlKey) { // when ctrl is pressed all nodes with the same aspect as the selected one should be highlighted @@ -41,4 +34,5 @@ export class StpaMouseListener extends MouseListener { } this.flaggedElements = []; } -} + +} \ No newline at end of file From bd53aa940f7cbd3a792b3133fff0ec2cbb32f046 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 8 Nov 2023 16:13:15 +0100 Subject: [PATCH 25/33] context menu works also for gates with descriptions --- .../context-menu-mouse-listener.ts | 2 +- .../context-menu/context-menu-provider.ts | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/extension/src-webview/context-menu/context-menu-mouse-listener.ts b/extension/src-webview/context-menu/context-menu-mouse-listener.ts index 5177a247..c40633e5 100644 --- a/extension/src-webview/context-menu/context-menu-mouse-listener.ts +++ b/extension/src-webview/context-menu/context-menu-mouse-listener.ts @@ -16,7 +16,7 @@ */ -import { ContextMenuMouseListener, SModelElement, SLabel } from "sprotty"; +import { ContextMenuMouseListener, SLabel, SModelElement } from "sprotty"; export class PastaContextMenuMouseListener extends ContextMenuMouseListener { diff --git a/extension/src-webview/context-menu/context-menu-provider.ts b/extension/src-webview/context-menu/context-menu-provider.ts index 71240c0d..fef2731a 100644 --- a/extension/src-webview/context-menu/context-menu-provider.ts +++ b/extension/src-webview/context-menu/context-menu-provider.ts @@ -26,15 +26,25 @@ export class ContextMenuProvider implements IContextMenuItemProvider { getItems(root: Readonly, lastMousePosition?: Point | undefined): Promise { if (root.type === FTA_GRAPH_TYPE) { // find node that was clicked on - const clickedNode = root.children.find(child => { + let clickedNode: FTANode | undefined; + + root.children.forEach((child) => { if (child.type === FTA_NODE_TYPE) { - return (child as FTANode).selected; + if ((child as FTANode).selected) { + clickedNode = child as FTANode; + } else { + const children = child.children.filter((child) => child.type === FTA_NODE_TYPE); + const selectedChild = children.find(child => (child as FTANode).selected); + if (selectedChild) { + clickedNode = selectedChild as FTANode; + } + } } }); return Promise.resolve([ { label: "Cut Set Analysis", - actions: [{ kind: "cutSetAnalysis", startId: clickedNode?.id} as CutSetAnalysisAction] + actions: [{ kind: "cutSetAnalysis", startId: clickedNode?.id } as CutSetAnalysisAction], } as LabeledAction, // { // label: "Minimal Cut Set Analysis", @@ -45,5 +55,4 @@ export class ContextMenuProvider implements IContextMenuItemProvider { return Promise.resolve([]); } } - -} \ No newline at end of file +} From b5e4c5dac07f7d476ffa40f213737fa0c2ce24b0 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 8 Nov 2023 16:18:12 +0100 Subject: [PATCH 26/33] context menu: added minimal cut set action --- .../fta/analysis/fta-cutSet-calculator.ts | 4 ++-- .../fta/fta-message-handler.ts | 6 +++--- extension/src-webview/actions.ts | 15 +++++++++++++++ .../context-menu/context-menu-provider.ts | 10 +++++----- extension/src/actions.ts | 16 ++++++++++++++++ extension/src/extension.ts | 4 ++-- extension/src/wview.ts | 13 ++++++++++++- 7 files changed, 55 insertions(+), 13 deletions(-) diff --git a/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts b/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts index 419e6a9e..6ffdc5d9 100644 --- a/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts +++ b/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts @@ -36,9 +36,9 @@ export let topOfAnalysis: string | undefined; * @param allNodes All nodes in the fault tree. * @returns the minimal cut sets for a fault tree. */ -export function determineMinimalCutSets(allNodes: AstNode[]): Set[] { +export function determineMinimalCutSets(allNodes: AstNode[], startNode?: namedFtaElement): Set[] { // TODO: add minimal flag (could reduce computation cost) - const allCutSets = determineCutSetsForFT(allNodes); + const allCutSets = determineCutSetsForFT(allNodes, startNode); // Cut sets are minimal if removing one element destroys the cut set // If cut set contains another cut set from the array, remove it since it is not minimal diff --git a/extension/src-language-server/fta/fta-message-handler.ts b/extension/src-language-server/fta/fta-message-handler.ts index 6fe48414..44f72045 100644 --- a/extension/src-language-server/fta/fta-message-handler.ts +++ b/extension/src-language-server/fta/fta-message-handler.ts @@ -52,8 +52,8 @@ function addCutSetsHandler( connection.onRequest("cutSets/generate", async (content: {uri: string, startId?: string}) => { return cutSetsRequested(content.uri, ftaServices, sharedServices, false, content.startId); }); - connection.onRequest("cutSets/generateMinimal", async (uri: string) => { - return cutSetsRequested(uri, ftaServices, sharedServices, true); + connection.onRequest("cutSets/generateMinimal", async (content: {uri: string, startId?: string}) => { + return cutSetsRequested(content.uri, ftaServices, sharedServices, true, content.startId); }); connection.onRequest("cutSets/reset", () => { return resetCutSets(ftaServices); @@ -81,7 +81,7 @@ async function cutSetsRequested( nodes.push(model.topEvent); } const startNode = startId ? nodes.find((node) => node.name === startId) : undefined; - const cutSets = minimal ? determineMinimalCutSets(nodes) : determineCutSetsForFT(nodes, startNode); + const cutSets = minimal ? determineMinimalCutSets(nodes, startNode) : determineCutSetsForFT(nodes, startNode); // determine single points of failure const spofs: string[] = []; for (const cutSet of cutSets) { diff --git a/extension/src-webview/actions.ts b/extension/src-webview/actions.ts index f800a229..9c6ba6f1 100644 --- a/extension/src-webview/actions.ts +++ b/extension/src-webview/actions.ts @@ -106,4 +106,19 @@ export namespace CutSetAnalysisAction { startId, }; } +} + +export interface MinimalCutSetAnalysisAction extends Action { + kind: typeof MinimalCutSetAnalysisAction.KIND; + startId: string +} +export namespace MinimalCutSetAnalysisAction { + export const KIND = 'minimalCutSetAnalysis'; + + export function create(startId: string,): MinimalCutSetAnalysisAction { + return { + kind: KIND, + startId, + }; + } } \ No newline at end of file diff --git a/extension/src-webview/context-menu/context-menu-provider.ts b/extension/src-webview/context-menu/context-menu-provider.ts index fef2731a..62cc62df 100644 --- a/extension/src-webview/context-menu/context-menu-provider.ts +++ b/extension/src-webview/context-menu/context-menu-provider.ts @@ -19,7 +19,7 @@ import { injectable } from "inversify"; import { IContextMenuItemProvider, LabeledAction, SModelRoot } from "sprotty"; import { Point } from "sprotty-protocol"; import { FTANode, FTA_GRAPH_TYPE, FTA_NODE_TYPE } from "../fta/fta-model"; -import { CutSetAnalysisAction } from "../actions"; +import { CutSetAnalysisAction, MinimalCutSetAnalysisAction } from "../actions"; @injectable() export class ContextMenuProvider implements IContextMenuItemProvider { @@ -46,10 +46,10 @@ export class ContextMenuProvider implements IContextMenuItemProvider { label: "Cut Set Analysis", actions: [{ kind: "cutSetAnalysis", startId: clickedNode?.id } as CutSetAnalysisAction], } as LabeledAction, - // { - // label: "Minimal Cut Set Analysis", - // actions: [{ kind: "minimalCutSetAnalysis", startId: clickedNode?.id} as MinimalCutSetAnalysisAction] - // } as LabeledAction + { + label: "Minimal Cut Set Analysis", + actions: [{ kind: "minimalCutSetAnalysis", startId: clickedNode?.id} as MinimalCutSetAnalysisAction] + } as LabeledAction ]); } else { return Promise.resolve([]); diff --git a/extension/src/actions.ts b/extension/src/actions.ts index 8ab8621c..436cb2e4 100644 --- a/extension/src/actions.ts +++ b/extension/src/actions.ts @@ -74,4 +74,20 @@ export namespace CutSetAnalysisAction { startId, }; } +} + + +export interface MinimalCutSetAnalysisAction extends Action { + kind: typeof MinimalCutSetAnalysisAction.KIND; + startId: string +} +export namespace MinimalCutSetAnalysisAction { + export const KIND = 'minimalCutSetAnalysis'; + + export function create(startId: string,): MinimalCutSetAnalysisAction { + return { + kind: KIND, + startId, + }; + } } \ No newline at end of file diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 8030666b..d0da7f72 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -219,8 +219,8 @@ function registerFTACommands(manager: StpaLspVscodeExtension, context: vscode.Ex }) ); context.subscriptions.push( - vscode.commands.registerCommand(options.extensionPrefix + ".fta.minimalCutSets", async (uri: vscode.Uri) => { - const minimalCutSets: string[] = await languageClient.sendRequest("cutSets/generateMinimal", uri.path); + vscode.commands.registerCommand(options.extensionPrefix + ".fta.minimalCutSets", async (uri: vscode.Uri, startId?: string) => { + const minimalCutSets: string[] = await languageClient.sendRequest("cutSets/generateMinimal", {uri: uri.path, startId}); await manager.openDiagram(uri); handleCutSets(manager, minimalCutSets, true); }) diff --git a/extension/src/wview.ts b/extension/src/wview.ts index 39446c4d..31780146 100644 --- a/extension/src/wview.ts +++ b/extension/src/wview.ts @@ -18,7 +18,7 @@ import { isActionMessage, SelectAction } from "sprotty-protocol"; import { LspWebviewEndpoint } from "sprotty-vscode/lib/lsp"; import * as vscode from "vscode"; -import { CutSetAnalysisAction, SendConfigAction } from "./actions"; +import { CutSetAnalysisAction, MinimalCutSetAnalysisAction, SendConfigAction } from "./actions"; export class StpaLspWebview extends LspWebviewEndpoint { receiveAction(message: any): Promise { @@ -35,6 +35,9 @@ export class StpaLspWebview extends LspWebviewEndpoint { case CutSetAnalysisAction.KIND: this.handleCutSetAnalysisAction(message.action as CutSetAnalysisAction); break; + case MinimalCutSetAnalysisAction.KIND: + this.handleMinimalCutSetAnalysisAction(message.action as MinimalCutSetAnalysisAction); + break; } } return super.receiveAction(message); @@ -81,6 +84,14 @@ export class StpaLspWebview extends LspWebviewEndpoint { } } + protected handleMinimalCutSetAnalysisAction(action: MinimalCutSetAnalysisAction): void { + const uriString = this.deserializeUriOfDiagramIdentifier(); + if (uriString !== "") { + const uri = vscode.Uri.parse(uriString); + vscode.commands.executeCommand("pasta.fta.minimalCutSets", uri, action.startId); + } + } + protected deserializeUriOfDiagramIdentifier(): string { if (this.diagramIdentifier) { let uriString = this.diagramIdentifier.uri.toString(); From df5b22c3f66b38ba366c46fca742d1b44a14a7de Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 8 Nov 2023 16:49:21 +0100 Subject: [PATCH 27/33] highlighting for top of analysis is not the root (WIP) --- extension/src-webview/fta/fta-views.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index 8a95c0d2..a5adbb73 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -20,7 +20,7 @@ import { injectable } from 'inversify'; import { VNode } from "snabbdom"; import { Hoverable, IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, SShapeElement, Selectable, svg } from 'sprotty'; import { renderAndGate, renderEllipse, renderHorizontalLine, renderInhibitGate, renderKnGate, renderOrGate, renderOval, renderRectangle, renderRoundedRectangle, renderVerticalLine } from "../views-rendering"; -import { DescriptionNode, FTAEdge, FTAGraph, FTANode, FTAPort, FTA_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType } from './fta-model'; +import { DescriptionNode, FTAEdge, FTAGraph, FTANode, FTAPort, FTA_DESCRIPTION_NODE_TYPE, FTA_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType } from './fta-model'; @injectable() export class PolylineArrowEdgeViewFTA extends PolylineEdgeView { @@ -130,8 +130,22 @@ export class FTAGraphView extends SGraphView { render(model: Readonly, context: RenderingContext): VNode { if (model.children.length !== 0) { - const topEvent = model.children.find(node => node instanceof FTANode && node.nodeType === FTNodeType.TOPEVENT); + const topEvent = model.children.find(node => node instanceof FTANode && node.topOfAnalysis); + // model.children.find(node => node instanceof FTANode && node.nodeType === FTNodeType.TOPEVENT); if (topEvent) { + // if a cut set is selected, for which the top event is not the root, + // we must hide the edges we dont inspect when calling "highlightConnectedToCutSet" + model.children.forEach(child => { + if (child.type === FTA_EDGE_TYPE) { + (child as FTAEdge).notConnectedToSelectedCutSet = true; + } else if (child.type === FTA_NODE_TYPE) { + (child as FTANode).children.forEach(child => { + if (child.type === FTA_EDGE_TYPE) { + (child as FTAEdge).notConnectedToSelectedCutSet = true; + } + }); + } + }); this.highlightConnectedToCutSet(model, topEvent as FTANode); } } @@ -164,7 +178,7 @@ export class FTAGraphView extends SGraphView { // handle nodes in parents if (currentNode.nodeType === FTNodeType.PARENT) { currentNode.children.forEach(child => { - if (child.type === FTA_NODE_TYPE) { + if (child.type === FTA_NODE_TYPE || child.type === FTA_DESCRIPTION_NODE_TYPE) { (child as FTANode).notConnectedToSelectedCutSet = currentNode.notConnectedToSelectedCutSet; } }); From 250d16600da6238b0acf6e9923cb39692210c9c4 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 9 Nov 2023 09:13:55 +0100 Subject: [PATCH 28/33] fixed spofs --- .../src-language-server/fta/diagram/fta-diagram-generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d891f30c..52cf1255 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -308,7 +308,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { if (set === spofsSet.id) { const spofs = this.options.getSpofs(); includedInCutSet = spofs.includes(node.name); - notConnected = false; + notConnected = !includedInCutSet; } const ftNode = this.createNode( From a8b5cbbc499bb5d2c36e6a297508b1f01b8e0a62 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 9 Nov 2023 11:14:03 +0100 Subject: [PATCH 29/33] fixed highlighting for gate with desc as top of analysis --- extension/src-webview/fta/fta-views.tsx | 26 ++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index a5adbb73..9fb3fb27 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -130,8 +130,23 @@ export class FTAGraphView extends SGraphView { render(model: Readonly, context: RenderingContext): VNode { if (model.children.length !== 0) { - const topEvent = model.children.find(node => node instanceof FTANode && node.topOfAnalysis); - // model.children.find(node => node instanceof FTANode && node.nodeType === FTNodeType.TOPEVENT); + + // find top event + let topEvent: FTANode | undefined; + model.children.forEach((child) => { + if (child.type === FTA_NODE_TYPE) { + if ((child as FTANode).topOfAnalysis) { + topEvent = child as FTANode; + } else { + const children = child.children.filter((child) => child.type === FTA_NODE_TYPE); + const toaChild = children.find(child => (child as FTANode).topOfAnalysis); + if (toaChild) { + topEvent = toaChild as FTANode; + } + } + } + }); + if (topEvent) { // if a cut set is selected, for which the top event is not the root, // we must hide the edges we dont inspect when calling "highlightConnectedToCutSet" @@ -146,7 +161,12 @@ export class FTAGraphView extends SGraphView { }); } }); - this.highlightConnectedToCutSet(model, topEvent as FTANode); + // highlight connected nodes + if (topEvent.parent.children.find(child => child.type === FTA_DESCRIPTION_NODE_TYPE)) { + this.highlightConnectedToCutSet(model, topEvent.parent as FTANode); + } else { + this.highlightConnectedToCutSet(model, topEvent as FTANode); + } } } From 944bc4cf2fde49146fd31bff4fef73019fdf5702 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 7 Dec 2023 11:39:15 +0100 Subject: [PATCH 30/33] translation of scenarios for UCAs to fta --- .../src-language-server/fta/diagram/fta-diagram-generator.ts | 2 +- extension/src-language-server/layout-engine.ts | 5 +++-- .../src-language-server/stpa/ftaGeneration/fta-generation.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index 52cf1255..db38e38c 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -302,7 +302,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { this.nodeToPort.set(nodeId, port); const description = isComponent(node) || isCondition(node) ? node.description : ""; const set = this.options.getCutSet(); - let includedInCutSet = set !== noCutSet.id ? set.includes(node.name) : false; + let includedInCutSet = set !== noCutSet.id ? set.includes(node.name + ",") || set.includes(node.name + "]") : false; let notConnected = set !== noCutSet.id ? !includedInCutSet : false; // single points of failure should be shown if (set === spofsSet.id) { diff --git a/extension/src-language-server/layout-engine.ts b/extension/src-language-server/layout-engine.ts index fc4a53bf..d6e1dddb 100644 --- a/extension/src-language-server/layout-engine.ts +++ b/extension/src-language-server/layout-engine.ts @@ -31,8 +31,9 @@ export class LayoutEngine extends ElkLayoutEngine { index.add(graph); } const elkGraph = this.transformToElk(graph, index) as ElkNode; - const debugElkGraph = JSON.stringify(elkGraph); - console.log(debugElkGraph); + /* used to inspect the elk graph in elklive */ + // const debugElkGraph = JSON.stringify(elkGraph); + // console.log(debugElkGraph); return this.elk.layout(elkGraph).then((result) => { this.applyLayout(result, index!); return graph; diff --git a/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts b/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts index abb46ebc..6bffe90e 100644 --- a/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts +++ b/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts @@ -17,7 +17,7 @@ import type { Reference } from "langium"; import { LangiumSprottySharedServices } from "langium-sprotty"; -import { Children, Component, Hazard, LossScenario, Model, ModelFTA, OR, TopEvent, isOR } from "../../generated/ast"; +import { Children, Component, Hazard, LossScenario, Model, ModelFTA, OR, TopEvent, UCA, isOR } from "../../generated/ast"; import { getModel } from "../../utils"; /** @@ -47,7 +47,7 @@ export async function createFaultTrees(uri: string, shared: LangiumSprottyShared function sortScenarios(model: Model): Map { const scenarios: Map = new Map(); for (const scenario of model.scenarios) { - const hazards = scenario.list?.refs; + const hazards = scenario.uca?.ref ? scenario.uca.ref.list.refs : scenario.list.refs; for (const hazard of hazards || []) { const hazardName = hazard.$refText; addToListMap(scenarios, hazardName, scenario); From 9e63afae4c5848709528fc320beeb80278cb7c3c Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 21 Dec 2023 11:08:39 +0100 Subject: [PATCH 31/33] Scenarios with no causal factors are considered --- .../src-language-server/stpa/ftaGeneration/fta-generation.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts b/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts index 6bffe90e..5983f0dc 100644 --- a/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts +++ b/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts @@ -80,6 +80,8 @@ function createFaulTreeForHazard(stpaModel: Model, scenarios: Map Date: Thu, 21 Dec 2023 15:55:03 +0100 Subject: [PATCH 32/33] added comments --- .../fta/analysis/fta-cutSet-calculator.ts | 1 + .../fta/diagram/fta-diagram-generator.ts | 3 + .../fta/diagram/fta-interfaces.ts | 6 + .../fta/diagram/fta-synthesis-options.ts | 12 ++ .../src-language-server/fta/diagram/utils.ts | 2 +- .../fta/fta-message-handler.ts | 5 +- extension/src-language-server/fta/utils.ts | 1 + .../stpa/ftaGeneration/fta-generation.ts | 17 +- extension/src-language-server/utils.ts | 9 + extension/src-webview/actions.ts | 4 +- .../context-menu/context-menu-provider.ts | 2 +- .../context-menu/context-menu-services.ts | 2 +- extension/src-webview/di.config.ts | 8 +- extension/src-webview/fta/fta-model.ts | 6 + extension/src-webview/fta/fta-views.tsx | 11 +- extension/src-webview/views-rendering.tsx | 14 ++ extension/src/actions.ts | 3 +- extension/src/extension.ts | 187 +++++++++++------- extension/src/wview.ts | 16 ++ 19 files changed, 216 insertions(+), 93 deletions(-) diff --git a/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts b/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts index 6ffdc5d9..abe13f54 100644 --- a/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts +++ b/extension/src-language-server/fta/analysis/fta-cutSet-calculator.ts @@ -29,6 +29,7 @@ import { } from "../../generated/ast"; import { namedFtaElement } from "../utils"; +/* element for which the cut sets were determined */ export let topOfAnalysis: string | undefined; /** 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 db38e38c..19dcf434 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -43,8 +43,11 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { /** Saves the Ids of the generated SNodes */ protected idToSNode: Map = new Map(); + /** Saves the parent node of a gate */ protected parentOfGate: Map = new Map(); + /** Saves the description node of a gate */ protected descriptionOfGate: Map = new Map(); + /** Saves the port of a node */ protected nodeToPort: Map = new Map(); constructor(services: FtaServices) { diff --git a/extension/src-language-server/fta/diagram/fta-interfaces.ts b/extension/src-language-server/fta/diagram/fta-interfaces.ts index 3c799b3f..eb2c9b99 100644 --- a/extension/src-language-server/fta/diagram/fta-interfaces.ts +++ b/extension/src-language-server/fta/diagram/fta-interfaces.ts @@ -32,10 +32,16 @@ export interface FTANode extends SNode { n?: number; } +/** + * FTA Graph. + */ export interface FTAGraph extends SGraph { modelOrder?: boolean; } +/** + * Description node of a fault tree. + */ export interface DescriptionNode extends SNode { name: string; inCurrentSelectedCutSet?: boolean; diff --git a/extension/src-language-server/fta/diagram/fta-synthesis-options.ts b/extension/src-language-server/fta/diagram/fta-synthesis-options.ts index d2204693..8ffc6dc6 100644 --- a/extension/src-language-server/fta/diagram/fta-synthesis-options.ts +++ b/extension/src-language-server/fta/diagram/fta-synthesis-options.ts @@ -48,6 +48,9 @@ const analysisCategoryOption: ValuedSynthesisOption = { currentValue: 0, }; +/** + * Boolean option to toggle the visualization gate descriptions. + */ const showGateDescriptionsOptions: ValuedSynthesisOption = { synthesisOption: { id: showGateDescriptionsID, @@ -61,6 +64,9 @@ const showGateDescriptionsOptions: ValuedSynthesisOption = { currentValue: true, }; +/** + * Boolean option to toggle the visualization node descriptions. + */ const showComponentDescriptionsOptions: ValuedSynthesisOption = { synthesisOption: { id: showComponentDescriptionsID, @@ -74,6 +80,9 @@ const showComponentDescriptionsOptions: ValuedSynthesisOption = { currentValue: false, }; +/** + * Option to highlight the components of a cut set. + */ const cutSets: ValuedSynthesisOption = { synthesisOption: { id: cutSetsID, @@ -123,6 +132,9 @@ export class FtaSynthesisOptions extends SynthesisOptions { } } + /** + * Resets the cutSets option to no cut set. + */ resetCutSets(): void { const option = this.getOption(cutSetsID); if (option) { diff --git a/extension/src-language-server/fta/diagram/utils.ts b/extension/src-language-server/fta/diagram/utils.ts index 71d008a0..55ffd5a7 100644 --- a/extension/src-language-server/fta/diagram/utils.ts +++ b/extension/src-language-server/fta/diagram/utils.ts @@ -59,7 +59,7 @@ export function getFTNodeType(node: AstNode): FTNodeType { */ export function getTargets(node: AstNode): AstNode[] { const targets: AstNode[] = []; - // only the top event and gates have children + // only the top event and gates have children/targets if (isTopEvent(node) && node.child.ref) { targets.push(node.child.ref); } diff --git a/extension/src-language-server/fta/fta-message-handler.ts b/extension/src-language-server/fta/fta-message-handler.ts index 44f72045..e8eff62d 100644 --- a/extension/src-language-server/fta/fta-message-handler.ts +++ b/extension/src-language-server/fta/fta-message-handler.ts @@ -15,7 +15,6 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { AstNode } from "langium"; import { LangiumSprottySharedServices } from "langium-sprotty"; import { Connection } from "vscode-languageserver"; import { ModelFTA } from "../generated/ast"; @@ -49,10 +48,10 @@ function addCutSetsHandler( ftaServices: FtaServices, sharedServices: LangiumSprottySharedServices ): void { - connection.onRequest("cutSets/generate", async (content: {uri: string, startId?: string}) => { + connection.onRequest("cutSets/generate", async (content: { uri: string; startId?: string }) => { return cutSetsRequested(content.uri, ftaServices, sharedServices, false, content.startId); }); - connection.onRequest("cutSets/generateMinimal", async (content: {uri: string, startId?: string}) => { + connection.onRequest("cutSets/generateMinimal", async (content: { uri: string; startId?: string }) => { return cutSetsRequested(content.uri, ftaServices, sharedServices, true, content.startId); }); connection.onRequest("cutSets/reset", () => { diff --git a/extension/src-language-server/fta/utils.ts b/extension/src-language-server/fta/utils.ts index 7e6081b2..2dc8fb84 100644 --- a/extension/src-language-server/fta/utils.ts +++ b/extension/src-language-server/fta/utils.ts @@ -18,6 +18,7 @@ import { Range } from "vscode-languageserver"; import { Component, Condition, Gate, ModelFTA, TopEvent, isAND, isInhibitGate, isKNGate, isOR } from "../generated/ast"; +/** FTA elements that have a name. */ export type namedFtaElement = Component | Condition | Gate | TopEvent; /** diff --git a/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts b/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts index 5983f0dc..229dc65d 100644 --- a/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts +++ b/extension/src-language-server/stpa/ftaGeneration/fta-generation.ts @@ -17,7 +17,7 @@ import type { Reference } from "langium"; import { LangiumSprottySharedServices } from "langium-sprotty"; -import { Children, Component, Hazard, LossScenario, Model, ModelFTA, OR, TopEvent, UCA, isOR } from "../../generated/ast"; +import { Children, Component, Hazard, LossScenario, Model, ModelFTA, OR, TopEvent } from "../../generated/ast"; import { getModel } from "../../utils"; /** @@ -34,7 +34,7 @@ export async function createFaultTrees(uri: string, shared: LangiumSprottyShared const scenarios: Map = sortScenarios(model); // create fault tree for each hazard for (const hazard of model.hazards) { - faultTrees.push(createFaulTreeForHazard(model, scenarios, hazard)); + faultTrees.push(createFaulTreeForHazard(scenarios, hazard)); } return faultTrees; } @@ -58,11 +58,10 @@ function sortScenarios(model: Model): Map { /** * Creates a fault tree with {@code hazard} as top event. - * @param stpaModel The stpa model that contains the {@code hazard}. * @param hazard The hazard for which the fault tree should be created. * @returns the AST of the created fault tree with {@code hazard} as top event. */ -function createFaulTreeForHazard(stpaModel: Model, scenarios: Map, hazard: Hazard): ModelFTA { +function createFaulTreeForHazard(scenarios: Map, hazard: Hazard): ModelFTA { const ftaModel = {} as ModelFTA; ftaModel.components = []; ftaModel.gates = []; @@ -108,7 +107,9 @@ function createFaulTreeForHazard(stpaModel: Model, scenarios: Map { return { ref: gate, $refText: gate.name } as Reference; }); + const gateChildren = ftaModel.gates.map((gate) => { + return { ref: gate, $refText: gate.name } as Reference; + }); const gate = { name: `G0`, children: gateChildren, @@ -129,6 +130,12 @@ function createFaulTreeForHazard(stpaModel: Model, scenarios: Map, key: string, value: any): void { if (map.has(key)) { const currentValues = map.get(key); diff --git a/extension/src-language-server/utils.ts b/extension/src-language-server/utils.ts index 78028a2a..b2f6f805 100644 --- a/extension/src-language-server/utils.ts +++ b/extension/src-language-server/utils.ts @@ -36,6 +36,15 @@ export async function getModel( return currentDoc.parseResult.value; } +/** + * Creates a list of labels containing the given {@code description} respecting the {@code labelManagement} and {@code labelWidth}. + * @param description The text for the label to create. + * @param labelManagement The label management option. + * @param labelWidth The desired width of the label. + * @param nodeId The id of the node for which the label is created. + * @param idCache The id cache. + * @returns a list of labels containing the given {@code description} respecting the {@code labelManagement} and {@code labelWidth}. + */ export function getDescription( description: string, labelManagement: labelManagementValue, diff --git a/extension/src-webview/actions.ts b/extension/src-webview/actions.ts index 9c6ba6f1..7979ecd8 100644 --- a/extension/src-webview/actions.ts +++ b/extension/src-webview/actions.ts @@ -17,7 +17,7 @@ import { inject } from "inversify"; import { CommandExecutionContext, CommandResult, HiddenCommand, TYPES, isExportable, isHoverable, isSelectable, isViewport } from "sprotty"; -import { RequestAction, ResponseAction, generateRequestId, Action } from "sprotty-protocol"; +import { Action, RequestAction, ResponseAction, generateRequestId } from "sprotty-protocol"; /** Requests the current SVG from the client. */ @@ -93,6 +93,7 @@ export class SvgCommand extends HiddenCommand { } } +/** Send from client to server to start a cut set analysis with the start node given by the startId */ export interface CutSetAnalysisAction extends Action { kind: typeof CutSetAnalysisAction.KIND; startId: string @@ -108,6 +109,7 @@ export namespace CutSetAnalysisAction { } } +/** Send from client to server to start a minimal cut set analysis with the start node given by the startId */ export interface MinimalCutSetAnalysisAction extends Action { kind: typeof MinimalCutSetAnalysisAction.KIND; startId: string diff --git a/extension/src-webview/context-menu/context-menu-provider.ts b/extension/src-webview/context-menu/context-menu-provider.ts index 62cc62df..1659aa24 100644 --- a/extension/src-webview/context-menu/context-menu-provider.ts +++ b/extension/src-webview/context-menu/context-menu-provider.ts @@ -27,7 +27,6 @@ export class ContextMenuProvider implements IContextMenuItemProvider { if (root.type === FTA_GRAPH_TYPE) { // find node that was clicked on let clickedNode: FTANode | undefined; - root.children.forEach((child) => { if (child.type === FTA_NODE_TYPE) { if ((child as FTANode).selected) { @@ -41,6 +40,7 @@ export class ContextMenuProvider implements IContextMenuItemProvider { } } }); + // create context menu items return Promise.resolve([ { label: "Cut Set Analysis", diff --git a/extension/src-webview/context-menu/context-menu-services.ts b/extension/src-webview/context-menu/context-menu-services.ts index ebf7a5f7..7722b5eb 100644 --- a/extension/src-webview/context-menu/context-menu-services.ts +++ b/extension/src-webview/context-menu/context-menu-services.ts @@ -15,7 +15,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { injectable, inject } from "inversify"; +import { inject, injectable } from "inversify"; import { Anchor, IContextMenuService, MenuItem } from "sprotty"; import { ActionNotification } from "sprotty-vscode-protocol"; import { VsCodeMessenger } from "sprotty-vscode-webview/lib/services"; diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index 1471e0ea..eff02c2d 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -34,13 +34,14 @@ import { TYPES, configureCommand, configureModelElement, + contextMenuModule, loadDefaultModules, - overrideViewerOptions, - contextMenuModule + overrideViewerOptions } from "sprotty"; import { SvgCommand } from "./actions"; -import { ContextMenuService } from "./context-menu/context-menu-services"; import { ContextMenuProvider } from "./context-menu/context-menu-provider"; +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"; @@ -72,7 +73,6 @@ import { STPAGraphView, STPANodeView, } from "./stpa/stpa-views"; -import pastaContextMenuModule from "./context-menu/di.config"; const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); diff --git a/extension/src-webview/fta/fta-model.ts b/extension/src-webview/fta/fta-model.ts index bdb7c4b4..9df301b7 100644 --- a/extension/src-webview/fta/fta-model.ts +++ b/extension/src-webview/fta/fta-model.ts @@ -60,10 +60,16 @@ export class FTANode extends SNode { n?: number; } +/** + * FTA Graph. + */ export class FTAGraph extends SGraph { modelOrder?: boolean; } +/** + * Description node of a fault tree. + */ export class DescriptionNode extends SNode { static readonly DEFAULT_FEATURES = [ connectableFeature, diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index 9fb3fb27..eb10db6d 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -18,8 +18,8 @@ /** @jsx svg */ import { injectable } from 'inversify'; import { VNode } from "snabbdom"; -import { Hoverable, IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, SShapeElement, Selectable, svg } from 'sprotty'; -import { renderAndGate, renderEllipse, renderHorizontalLine, renderInhibitGate, renderKnGate, renderOrGate, renderOval, renderRectangle, renderRoundedRectangle, renderVerticalLine } from "../views-rendering"; +import { IViewArgs, Point, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdge, SGraph, SGraphView, svg } from 'sprotty'; +import { renderAndGate, renderEllipse, renderHorizontalLine, renderInhibitGate, renderKnGate, renderOrGate, renderRectangle, renderRoundedRectangle, renderVerticalLine } from "../views-rendering"; import { DescriptionNode, FTAEdge, FTAGraph, FTANode, FTAPort, FTA_DESCRIPTION_NODE_TYPE, FTA_EDGE_TYPE, FTA_NODE_TYPE, FTA_PORT_TYPE, FTNodeType } from './fta-model'; @injectable() @@ -56,6 +56,7 @@ export class FTAInvisibleEdgeView extends PolylineArrowEdgeViewFTA { @injectable() export class DescriptionNodeView extends RectangularNodeView { render(node: DescriptionNode, context: RenderingContext): VNode | undefined { + // render the description node similar to an on edge label const element = renderRectangle(node); const border1 = renderHorizontalLine(node); const border2 = renderHorizontalLine(node); @@ -172,7 +173,11 @@ export class FTAGraphView extends SGraphView { return super.render(model, context); } - + /** + * Highlights the nodes and edges connected to the selected cut set. + * @param model The FTAGraph. + * @param currentNode The current node, which should be handled including its targets. + */ protected highlightConnectedToCutSet(model: SGraph, currentNode: FTANode): void { for (const port of currentNode.children.filter(child => child.type === FTA_PORT_TYPE)) { const edge = model.children.find(child => child.type === FTA_EDGE_TYPE && (child as FTAEdge).sourceId === port.id) as FTAEdge; diff --git a/extension/src-webview/views-rendering.tsx b/extension/src-webview/views-rendering.tsx index 2849fe9e..12003c89 100644 --- a/extension/src-webview/views-rendering.tsx +++ b/extension/src-webview/views-rendering.tsx @@ -32,6 +32,15 @@ export function renderOval(node: SNode): VNode { ry={Math.max(node.size.height, 0) / 2.0} />; } +/** + * Creates an ellipse for {@code node}. + * @param x The x-coordinate of the ellipse. + * @param y The y-coordinate of the ellipse. + * @param width The width of the ellipse. + * @param height The height of the ellipse. + * @param lineWidth The line width of the ellipse. + * @returns an ellipse for {@code node}. + */ export function renderEllipse(x: number | undefined, y: number | undefined, width: number, height: number, lineWidth: number): VNode { return { } } -function registerSTPACommands(manager: StpaLspVscodeExtension, context: vscode.ExtensionContext, options: { extensionPrefix: string; }): void { +/** + * Register all commands that are specific to STPA. + * @param manager The manager that handles the webview panels. + * @param context The context of the extension. + * @param options The options for the commands. + */ +function registerSTPACommands( + manager: StpaLspVscodeExtension, + context: vscode.ExtensionContext, + options: { extensionPrefix: string } +): void { context.subscriptions.push( vscode.commands.registerCommand( options.extensionPrefix + ".contextTable.open", @@ -209,65 +222,93 @@ function registerSTPACommands(manager: StpaLspVscodeExtension, context: vscode.E ); } -function registerFTACommands(manager: StpaLspVscodeExtension, context: vscode.ExtensionContext, options: { extensionPrefix: string; }): void { +/** + * Register all commands that are specific to FTA. + * @param manager The manager that handles the webview panels. + * @param context The context of the extension. + * @param options The options for the commands. + */ +function registerFTACommands( + manager: StpaLspVscodeExtension, + context: vscode.ExtensionContext, + options: { extensionPrefix: string } +): void { // commands for computing and displaying the (minimal) cut sets of the fault tree. context.subscriptions.push( - vscode.commands.registerCommand(options.extensionPrefix + ".fta.cutSets", async (uri: vscode.Uri, startId?: string) => { - const cutSets: string[] = await languageClient.sendRequest("cutSets/generate", {uri: uri.path, startId}); - await manager.openDiagram(uri); - handleCutSets(manager, cutSets, false); - }) + vscode.commands.registerCommand( + options.extensionPrefix + ".fta.cutSets", + async (uri: vscode.Uri, startId?: string) => { + const cutSets: string[] = await languageClient.sendRequest("cutSets/generate", { + uri: uri.path, + startId, + }); + await manager.openDiagram(uri); + handleCutSets(cutSets, false); + } + ) ); context.subscriptions.push( - vscode.commands.registerCommand(options.extensionPrefix + ".fta.minimalCutSets", async (uri: vscode.Uri, startId?: string) => { - const minimalCutSets: string[] = await languageClient.sendRequest("cutSets/generateMinimal", {uri: uri.path, startId}); - await manager.openDiagram(uri); - handleCutSets(manager, minimalCutSets, true); - }) + vscode.commands.registerCommand( + options.extensionPrefix + ".fta.minimalCutSets", + async (uri: vscode.Uri, startId?: string) => { + const minimalCutSets: string[] = await languageClient.sendRequest("cutSets/generateMinimal", { + uri: uri.path, + startId, + }); + await manager.openDiagram(uri); + handleCutSets(minimalCutSets, true); + } + ) ); } -function handleCutSets(manager: StpaLspVscodeExtension, cutSets: string[], minimal?: boolean): void { +/** + * Handles the result of the cut set analysis. + * @param manager The manager that handles the webview panels. + * @param cutSets The cut sets that should be handled. + * @param minimal Determines whether the cut sets are minimal or not. + */ +function handleCutSets(cutSets: string[], minimal?: boolean): void { // print cut sets to output channel createOutputChannel(cutSets, "FTA Cut Sets", minimal); } function createLanguageClient(context: vscode.ExtensionContext): LanguageClient { - const serverModule = context.asAbsolutePath(path.join('pack', 'language-server')); + const serverModule = context.asAbsolutePath(path.join("pack", "language-server")); // The debug options for the server // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging. // By setting `process.env.DEBUG_BREAK` to a truthy value, the language server will wait until a debugger is attached. - const debugOptions = { execArgv: ['--nolazy', `--inspect${process.env.DEBUG_BREAK ? '-brk' : ''}=${process.env.DEBUG_SOCKET || '6009'}`] }; + const debugOptions = { + execArgv: [ + "--nolazy", + `--inspect${process.env.DEBUG_BREAK ? "-brk" : ""}=${process.env.DEBUG_SOCKET || "6009"}`, + ], + }; // If the extension is launched in debug mode then the debug server options are used // Otherwise the run options are used const serverOptions: ServerOptions = { run: { module: serverModule, transport: TransportKind.ipc }, - debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } + debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }, }; - const fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/*.{stpa,fta}'); + const fileSystemWatcher = vscode.workspace.createFileSystemWatcher("**/*.{stpa,fta}"); context.subscriptions.push(fileSystemWatcher); // Options to control the language client const clientOptions: LanguageClientOptions = { documentSelector: supportedFileEndings.map((ending) => ({ - scheme: 'file', + scheme: "file", language: ending, })), synchronize: { // Notify the server about file changes to files contained in the workspace - fileEvents: fileSystemWatcher - } + fileEvents: fileSystemWatcher, + }, }; // Create the language client and start the client. - const languageClient = new LanguageClient( - 'stpa', - 'stpa', - serverOptions, - clientOptions - ); + const languageClient = new LanguageClient("stpa", "stpa", serverOptions, clientOptions); // Start the client. This will also launch the server languageClient.start(); @@ -276,21 +317,21 @@ function createLanguageClient(context: vscode.ExtensionContext): LanguageClient function registerTextEditorSync(manager: StpaLspVscodeExtension, context: vscode.ExtensionContext): void { context.subscriptions.push( - vscode.workspace.onDidSaveTextDocument(async document => { + vscode.workspace.onDidSaveTextDocument(async (document) => { if (document) { manager.openDiagram(document.uri); } }) ); context.subscriptions.push( - vscode.workspace.onDidSaveTextDocument(async document => { + vscode.workspace.onDidSaveTextDocument(async (document) => { if (document) { - await languageClient.sendRequest('cutSets/reset'); + await languageClient.sendRequest("cutSets/reset"); manager.openDiagram(document.uri); if (manager.contextTable) { - languageClient.sendNotification('contextTable/getData', document.uri.toString()); + languageClient.sendNotification("contextTable/getData", document.uri.toString()); } } }) ); -} \ No newline at end of file +} diff --git a/extension/src/wview.ts b/extension/src/wview.ts index 31780146..9a54064a 100644 --- a/extension/src/wview.ts +++ b/extension/src/wview.ts @@ -71,11 +71,19 @@ export class StpaLspWebview extends LspWebviewEndpoint { this.sendAction({ kind: SendConfigAction.KIND, options: renderOptions } as SendConfigAction); } + /** + * Updates the configuration of the PASTA extension. + * @param action The action containing the configuration options. + */ protected updateConfigValues(action: SendConfigAction): void { const configOptions = vscode.workspace.getConfiguration("pasta"); action.options.forEach((element) => configOptions.update(element.id, element.value)); } + /** + * Executes the cut set analysis for the given start ID. + * @param action The action containing the start ID. + */ protected handleCutSetAnalysisAction(action: CutSetAnalysisAction): void { const uriString = this.deserializeUriOfDiagramIdentifier(); if (uriString !== "") { @@ -84,6 +92,10 @@ export class StpaLspWebview extends LspWebviewEndpoint { } } + /** + * Executes the minimal cut set analysis for the given start ID. + * @param action The action containing the start ID. + */ protected handleMinimalCutSetAnalysisAction(action: MinimalCutSetAnalysisAction): void { const uriString = this.deserializeUriOfDiagramIdentifier(); if (uriString !== "") { @@ -92,6 +104,10 @@ export class StpaLspWebview extends LspWebviewEndpoint { } } + /** + * Deserializes the URI of the diagram identifier. + * @returns the deserialized URI of the diagram identifier. + */ protected deserializeUriOfDiagramIdentifier(): string { if (this.diagramIdentifier) { let uriString = this.diagramIdentifier.uri.toString(); From 3e15ad8bb8ee42093b32d1425e25b17419d2ff43 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 22 Dec 2023 11:02:19 +0100 Subject: [PATCH 33/33] mka feedback --- .../fta/diagram/fta-diagram-generator.ts | 3 +-- .../fta/diagram/fta-layout-config.ts | 17 +++-------------- .../fta/diagram/fta-model.ts | 2 +- extension/src-language-server/layout-engine.ts | 12 ++++++------ .../src-language-server/stpa/stpa-module.ts | 5 ++--- .../src-language-server/synthesis-options.ts | 2 +- extension/src-webview/actions.ts | 2 +- .../context-menu/context-menu-mouse-listener.ts | 2 +- .../context-menu/context-menu-provider.ts | 2 +- extension/src-webview/css/context-menu.css | 2 +- extension/src-webview/css/fta-diagram.css | 2 +- extension/src-webview/fta/fta-views.tsx | 2 +- extension/src/actions.ts | 2 +- 13 files changed, 21 insertions(+), 34 deletions(-) diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index 19dcf434..09d2ac00 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -42,7 +42,6 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { /** Saves the Ids of the generated SNodes */ protected idToSNode: Map = new Map(); - /** Saves the parent node of a gate */ protected parentOfGate: Map = new Map(); /** Saves the description node of a gate */ @@ -117,7 +116,7 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { sourceNode?.children?.push(this.createFTAPort(sourcePortId, PortSide.SOUTH)); // create port for source parent and edge to this port - let sourceParentPortId: string | undefined = undefined; + let sourceParentPortId: string | undefined; if (this.parentOfGate.has(sourceId)) { const parent = this.parentOfGate.get(sourceId); sourceParentPortId = idCache.uniqueId(edgeId + "_port"); diff --git a/extension/src-language-server/fta/diagram/fta-layout-config.ts b/extension/src-language-server/fta/diagram/fta-layout-config.ts index cfcd3a8f..5e45594b 100644 --- a/extension/src-language-server/fta/diagram/fta-layout-config.ts +++ b/extension/src-language-server/fta/diagram/fta-layout-config.ts @@ -70,22 +70,11 @@ export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { return options; } - protected portOptions(sport: FTAPort, index: SModelIndex): LayoutOptions | undefined { + protected portOptions(sport: FTAPort, _index: SModelIndex): LayoutOptions | undefined { if (sport.type === FTA_PORT_TYPE) { let side = ""; - switch ((sport as FTAPort).side) { - case PortSide.WEST: - side = "WEST"; - break; - case PortSide.EAST: - side = "EAST"; - break; - case PortSide.NORTH: - side = "NORTH"; - break; - case PortSide.SOUTH: - side = "SOUTH"; - break; + if ((sport as FTAPort).side) { + side = PortSide[(sport as FTAPort).side!].toUpperCase(); } return { "org.eclipse.elk.port.side": side, diff --git a/extension/src-language-server/fta/diagram/fta-model.ts b/extension/src-language-server/fta/diagram/fta-model.ts index ada3788a..bf961775 100644 --- a/extension/src-language-server/fta/diagram/fta-model.ts +++ b/extension/src-language-server/fta/diagram/fta-model.ts @@ -45,4 +45,4 @@ export enum PortSide { EAST, NORTH, SOUTH -} \ No newline at end of file +} diff --git a/extension/src-language-server/layout-engine.ts b/extension/src-language-server/layout-engine.ts index d6e1dddb..3fed65d5 100644 --- a/extension/src-language-server/layout-engine.ts +++ b/extension/src-language-server/layout-engine.ts @@ -48,13 +48,13 @@ export class LayoutEngine extends ElkLayoutEngine { } if (elkEdge.sections && elkEdge.sections.length > 0) { const section = elkEdge.sections[0]; - if (section.startPoint) points.push(section.startPoint); - if (section.bendPoints) points.push(...section.bendPoints); - if (section.endPoint) points.push(section.endPoint); + if (section.startPoint) { points.push(section.startPoint); } + if (section.bendPoints) { points.push(...section.bendPoints); } + if (section.endPoint) { points.push(section.endPoint); } } else if (isPrimitiveEdge(elkEdge)) { - if (elkEdge.sourcePoint) points.push(elkEdge.sourcePoint); - if (elkEdge.bendPoints) points.push(...elkEdge.bendPoints); - if (elkEdge.targetPoint) points.push(elkEdge.targetPoint); + if (elkEdge.sourcePoint) { points.push(elkEdge.sourcePoint); } + if (elkEdge.bendPoints) { points.push(...elkEdge.bendPoints); } + if (elkEdge.targetPoint) { points.push(elkEdge.targetPoint); } } sedge.routingPoints = points; diff --git a/extension/src-language-server/stpa/stpa-module.ts b/extension/src-language-server/stpa/stpa-module.ts index 84547296..e6581f8b 100644 --- a/extension/src-language-server/stpa/stpa-module.ts +++ b/extension/src-language-server/stpa/stpa-module.ts @@ -27,10 +27,10 @@ import { import { DefaultElementFilter, ElkFactory, - ElkLayoutEngine, IElementFilter, - ILayoutConfigurator, + ILayoutConfigurator } from "sprotty-elk/lib/elk-layout"; +import { LayoutEngine } from "../layout-engine"; import { IDEnforcer } from "./ID-enforcer"; import { ContextTableProvider } from "./contextTable/context-dataProvider"; import { StpaDiagramGenerator } from "./diagram/diagram-generator"; @@ -38,7 +38,6 @@ import { StpaLayoutConfigurator } from "./diagram/layout-config"; import { StpaSynthesisOptions } from "./diagram/stpa-synthesis-options"; import { StpaScopeProvider } from "./stpa-scopeProvider"; import { StpaValidationRegistry, StpaValidator } from "./stpa-validator"; -import { LayoutEngine } from "../layout-engine"; /** * Declaration of custom services - add your own service classes here. diff --git a/extension/src-language-server/synthesis-options.ts b/extension/src-language-server/synthesis-options.ts index fffa9ff8..48212797 100644 --- a/extension/src-language-server/synthesis-options.ts +++ b/extension/src-language-server/synthesis-options.ts @@ -147,4 +147,4 @@ export class SynthesisOptions { getModelOrder(): boolean { return this.getOption(modelOrderID)?.currentValue; } -} \ No newline at end of file +} diff --git a/extension/src-webview/actions.ts b/extension/src-webview/actions.ts index 7979ecd8..139c597d 100644 --- a/extension/src-webview/actions.ts +++ b/extension/src-webview/actions.ts @@ -123,4 +123,4 @@ export namespace MinimalCutSetAnalysisAction { startId, }; } -} \ No newline at end of file +} diff --git a/extension/src-webview/context-menu/context-menu-mouse-listener.ts b/extension/src-webview/context-menu/context-menu-mouse-listener.ts index c40633e5..926e42f6 100644 --- a/extension/src-webview/context-menu/context-menu-mouse-listener.ts +++ b/extension/src-webview/context-menu/context-menu-mouse-listener.ts @@ -27,4 +27,4 @@ export class PastaContextMenuMouseListener extends ContextMenuMouseListener { super.showContextMenu(target, event); } } -} \ No newline at end of file +} diff --git a/extension/src-webview/context-menu/context-menu-provider.ts b/extension/src-webview/context-menu/context-menu-provider.ts index 1659aa24..2f68df95 100644 --- a/extension/src-webview/context-menu/context-menu-provider.ts +++ b/extension/src-webview/context-menu/context-menu-provider.ts @@ -23,7 +23,7 @@ import { CutSetAnalysisAction, MinimalCutSetAnalysisAction } from "../actions"; @injectable() export class ContextMenuProvider implements IContextMenuItemProvider { - getItems(root: Readonly, lastMousePosition?: Point | undefined): Promise { + getItems(root: Readonly, _lastMousePosition?: Point | undefined): Promise { if (root.type === FTA_GRAPH_TYPE) { // find node that was clicked on let clickedNode: FTANode | undefined; diff --git a/extension/src-webview/css/context-menu.css b/extension/src-webview/css/context-menu.css index 0b092fbb..c9f1a931 100644 --- a/extension/src-webview/css/context-menu.css +++ b/extension/src-webview/css/context-menu.css @@ -29,4 +29,4 @@ color: var(--vscode-menu-selectionForeground); } -/* --vscode-menu-separatorBackground: #d4d4d4; */ \ No newline at end of file +/* --vscode-menu-separatorBackground: #d4d4d4; */ diff --git a/extension/src-webview/css/fta-diagram.css b/extension/src-webview/css/fta-diagram.css index a94d0ee8..cf3b96b8 100644 --- a/extension/src-webview/css/fta-diagram.css +++ b/extension/src-webview/css/fta-diagram.css @@ -64,4 +64,4 @@ .top-event { stroke: dodgerblue; -} \ No newline at end of file +} diff --git a/extension/src-webview/fta/fta-views.tsx b/extension/src-webview/fta/fta-views.tsx index eb10db6d..6e2360ad 100644 --- a/extension/src-webview/fta/fta-views.tsx +++ b/extension/src-webview/fta/fta-views.tsx @@ -210,4 +210,4 @@ export class FTAGraphView extends SGraphView { } } -} \ No newline at end of file +} diff --git a/extension/src/actions.ts b/extension/src/actions.ts index 9335a4ee..0b283813 100644 --- a/extension/src/actions.ts +++ b/extension/src/actions.ts @@ -91,4 +91,4 @@ export namespace MinimalCutSetAnalysisAction { startId, }; } -} \ No newline at end of file +}