import {ParameterTypeModel, StepModel, WorkflowInputParameterModel, WorkflowOutputParameterModel} from "cwlts/models"; import {HtmlUtils} from "../utils/html-utils"; import {SVGUtils} from "../utils/svg-utils"; import {IOPort} from "./io-port"; export type NodePosition = { x: number, y: number }; export type NodeDataModel = WorkflowInputParameterModel | WorkflowOutputParameterModel | StepModel; export class GraphNode { public position: NodePosition = {x: 0, y: 0}; static radius = 30; constructor(position: Partial, private dataModel: NodeDataModel) { this.dataModel = dataModel; Object.assign(this.position, position); } /** * @FIXME Making icons increases the rendering time by 50-100%. Try embedding the SVG directly. */ private static workflowIconSvg: string = "workflow"; private static toolIconSvg: string = "tool2"; private static fileInputIconSvg: string = "file_input"; private static fileOutputIconSvg: string = "file_output"; private static inputIconSvg: string = "type_input"; private static outputIconSvg: string = "type_output"; private static makeIconFragment(model: any) { let iconStr = ""; if (model instanceof StepModel && model.run) { if (model.run.class === "Workflow") { iconStr = this.workflowIconSvg; } else if (model.run.class === "CommandLineTool") { iconStr = this.toolIconSvg; } } else if (model instanceof WorkflowInputParameterModel && model.type) { if (model.type.type === "File" || (model.type.type === "array" && model.type.items === "File")) { iconStr = this.fileInputIconSvg; } else { iconStr = this.inputIconSvg; } } else if (model instanceof WorkflowOutputParameterModel && model.type) { if (model.type.type === "File" || (model.type.type === "array" && model.type.items === "File")) { iconStr = this.fileOutputIconSvg; } else { iconStr = this.outputIconSvg; } } return iconStr; } static makeTemplate(dataModel: { id: string, connectionId: string, label?: string, in?: any[], type?: ParameterTypeModel out?: any[], customProps?: { "sbg:x"?: number "sbg:y"?: number } }, labelScale = 1): string { const x = ~~(dataModel.customProps && dataModel.customProps["sbg:x"])!; const y = ~~(dataModel.customProps && dataModel.customProps["sbg:y"])!; let nodeTypeClass = "step"; if (dataModel instanceof WorkflowInputParameterModel) { nodeTypeClass = "input"; } else if (dataModel instanceof WorkflowOutputParameterModel) { nodeTypeClass = "output"; } const inputs = (dataModel.in || []).filter(p => p.isVisible); const outputs = (dataModel.out || []).filter(p => p.isVisible); const maxPorts = Math.max(inputs.length, outputs.length); const radius = GraphNode.radius + maxPorts * IOPort.radius; let typeClass = ""; let itemsClass = ""; if (dataModel.type) { typeClass = "type-" + dataModel.type.type; if(dataModel.type.items){ itemsClass = "items-" + dataModel.type.items; } } const inputPortTemplates = inputs .sort((a, b) => -a.id.localeCompare(b.id)) .map((p, i, arr) => GraphNode.makePortTemplate( p, "input", SVGUtils.matrixToTransformAttr( GraphNode.createPortMatrix(arr.length, i, radius, "input") ) )) .reduce((acc, tpl) => acc + tpl, ""); const outputPortTemplates = outputs .sort((a, b) => -a.id.localeCompare(b.id)) .map((p, i, arr) => GraphNode.makePortTemplate( p, "output", SVGUtils.matrixToTransformAttr( GraphNode.createPortMatrix(arr.length, i, radius, "output") ) )) .reduce((acc, tpl) => acc + tpl, ""); return ` ${GraphNode.makeIconFragment(dataModel)} ${HtmlUtils.escapeHTML(dataModel.label || dataModel.id)} ${inputPortTemplates} ${outputPortTemplates} `; } private static makePortTemplate(port: { label?: string, id: string, connectionId: string }, type: "input" | "output", transform = "matrix(1, 0, 0, 1, 0, 0)"): string { const portClass = type === "input" ? "input-port" : "output-port"; const label = port.label || port.id; return ` ${label} `; } public static createPortMatrix(totalPortLength: number, portIndex: number, radius: number, type: "input" | "output"): SVGMatrix { const availableAngle = 140; let rotationAngle = // Starting rotation angle (-availableAngle / 2) + ( // Angular offset by element index (portIndex + 1) // Angle between elements * availableAngle / (totalPortLength + 1) ); if (type === "input") { rotationAngle = // Determines the starting rotation angle 180 - (availableAngle / -2) // Determines the angular offset modifier for the current index - (portIndex + 1) // Determines the angular offset * availableAngle / (totalPortLength + 1); } const matrix = SVGUtils.createMatrix(); return matrix.rotate(rotationAngle).translate(radius, 0).rotate(-rotationAngle); } static patchModelPorts(model: T & { connectionId: string, id: string }): T { const patch = [{connectionId: model.connectionId, isVisible: true, id: model.id}]; if (model instanceof WorkflowInputParameterModel) { const copy = Object.create(model); return Object.assign(copy, {out: patch}); } else if (model instanceof WorkflowOutputParameterModel) { const copy = Object.create(model); return Object.assign(copy, {in: patch}); } return model; } }