1 import {ParameterTypeModel, StepModel, WorkflowInputParameterModel, WorkflowOutputParameterModel} from "cwlts/models";
2 import {HtmlUtils} from "../utils/html-utils";
3 import {SVGUtils} from "../utils/svg-utils";
4 import {IOPort} from "./io-port";
6 export type NodePosition = { x: number, y: number };
7 export type NodeDataModel = WorkflowInputParameterModel | WorkflowOutputParameterModel | StepModel;
9 export class GraphNode {
11 public position: NodePosition = {x: 0, y: 0};
15 constructor(position: Partial<NodePosition>,
16 private dataModel: NodeDataModel) {
18 this.dataModel = dataModel;
20 Object.assign(this.position, position);
24 * @FIXME Making icons increases the rendering time by 50-100%. Try embedding the SVG directly.
27 private static workflowIconSvg: string = "<svg class=\"node-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 400.01 399.88\" x=\"-9\" y=\"-10\" width=\"20\" height=\"20\"><title>workflow</title><path d=\"M400,200a80,80,0,0,1-140.33,52.53L158.23,303.24a80,80,0,1,1-17.9-35.77l101.44-50.71a80.23,80.23,0,0,1,0-33.52L140.33,132.53a79.87,79.87,0,1,1,17.9-35.77l101.44,50.71A80,80,0,0,1,400,200Z\" transform=\"translate(0.01 -0.16)\"/></svg>";
28 private static toolIconSvg: string = "<svg class=\"node-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 398.39 397.78\" x=\"-10\" y=\"-8\" width=\"20\" height=\"15\"><title>tool2</title><polygon points=\"38.77 397.57 0 366 136.15 198.78 0 31.57 38.77 0 200.63 198.78 38.77 397.57\"/><rect x=\"198.39\" y=\"347.78\" width=\"200\" height=\"50\"/></svg>";
29 private static fileInputIconSvg: string = "<svg class=\"node-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 499 462.86\" y=\"-10\" x=\"-11\" width=\"20\" height=\"20\"><title>file_input</title><path d=\"M386.06,0H175V58.29l50,50V50H337.81V163.38h25l86.19.24V412.86H225V353.71l-50,50v59.15H499V112.94Zm1.75,113.45v-41l41.1,41.1Z\"/><polygon points=\"387.81 1.06 387.81 1.75 387.12 1.06 387.81 1.06\"/><polygon points=\"290.36 231 176.68 344.68 141.32 309.32 194.64 256 0 256 0 206 194.64 206 142.32 153.68 177.68 118.32 290.36 231\"/></svg>";
30 private static fileOutputIconSvg: string = "<svg class=\"node-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 499 462.86\" x=\"-7\" y=\"-11\" width=\"20\" height=\"20\"><title>file_output</title><polygon points=\"387.81 1.06 387.81 1.75 387.12 1.06 387.81 1.06\"/><polygon points=\"499 231 385.32 344.68 349.96 309.32 403.28 256 208.64 256 208.64 206 403.28 206 350.96 153.68 386.32 118.32 499 231\"/><path d=\"M187.81,163.38l77.69.22H324V112.94L211.06,0H0V462.86H324V298.5H274V412.86H50V50H162.81V163.38Zm25-90.92,41.1,41.1-41.1-.11Z\"/></svg>";
31 private static inputIconSvg: string = "<svg class=\"node-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 499 365\" x=\"-11\" y=\"-10\" width=\"20\" height=\"20\"><title>type_input</title><g id=\"input\"><path d=\"M316.5,68a181.72,181.72,0,0,0-114.12,40.09L238,143.72a132.5,132.5,0,1,1,1.16,214.39L203.48,393.8A182.5,182.5,0,1,0,316.5,68Z\" transform=\"translate(0 -68)\"/><g id=\"Layer_22\" data-name=\"Layer 22\"><g id=\"Layer_9_copy_4\" data-name=\"Layer 9 copy 4\"><polygon points=\"290.36 182 176.68 295.68 141.32 260.32 194.64 207 0 207 0 157 194.64 157 142.32 104.68 177.68 69.32 290.36 182\"/></g></g></g></svg>";
32 private static outputIconSvg: string = "<svg class=\"node-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 500.36 365\" x=\"-9\" y=\"-10\" width=\"20\" height=\"20\"><title>type_output</title><g id=\"output\"><path d=\"M291.95,325.23a134,134,0,0,1-15.76,19,132.5,132.5,0,1,1,0-187.38,133.9,133.9,0,0,1,16.16,19.55l35.81-35.81A182.5,182.5,0,1,0,327.73,361Z\" transform=\"translate(0 -68)\"/><g id=\"circle_source_copy\" data-name=\"circle source copy\"><g id=\"Layer_22_copy\" data-name=\"Layer 22 copy\"><g id=\"Layer_9_copy_5\" data-name=\"Layer 9 copy 5\"><polygon points=\"500.36 182 386.68 295.68 351.32 260.32 404.64 207 210 207 210 157 404.64 157 352.32 104.68 387.68 69.32 500.36 182\"/></g></g></g></g></svg>";
34 private static makeIconFragment(model: any) {
38 if (model instanceof StepModel && model.run) {
40 if (model.run.class === "Workflow") {
41 iconStr = this.workflowIconSvg;
42 } else if (model.run.class === "CommandLineTool") {
43 iconStr = this.toolIconSvg;
46 } else if (model instanceof WorkflowInputParameterModel && model.type) {
47 if (model.type.type === "File" || (model.type.type === "array" && model.type.items === "File")) {
48 iconStr = this.fileInputIconSvg;
50 iconStr = this.inputIconSvg;
52 } else if (model instanceof WorkflowOutputParameterModel && model.type) {
53 if (model.type.type === "File" || (model.type.type === "array" && model.type.items === "File")) {
54 iconStr = this.fileOutputIconSvg;
56 iconStr = this.outputIconSvg;
63 static makeTemplate(dataModel: {
68 type?: ParameterTypeModel
74 }, labelScale = 1): string {
76 const x = ~~(dataModel.customProps && dataModel.customProps["sbg:x"])!;
77 const y = ~~(dataModel.customProps && dataModel.customProps["sbg:y"])!;
79 let nodeTypeClass = "step";
80 if (dataModel instanceof WorkflowInputParameterModel) {
81 nodeTypeClass = "input";
82 } else if (dataModel instanceof WorkflowOutputParameterModel) {
83 nodeTypeClass = "output";
86 const inputs = (dataModel.in || []).filter(p => p.isVisible);
87 const outputs = (dataModel.out || []).filter(p => p.isVisible);
88 const maxPorts = Math.max(inputs.length, outputs.length);
89 const radius = GraphNode.radius + maxPorts * IOPort.radius;
95 typeClass = "type-" + dataModel.type.type;
97 if(dataModel.type.items){
98 itemsClass = "items-" + dataModel.type.items;
102 const inputPortTemplates = inputs
103 .sort((a, b) => -a.id.localeCompare(b.id))
104 .map((p, i, arr) => GraphNode.makePortTemplate(
107 SVGUtils.matrixToTransformAttr(
108 GraphNode.createPortMatrix(arr.length, i, radius, "input")
111 .reduce((acc, tpl) => acc + tpl, "");
113 const outputPortTemplates = outputs
114 .sort((a, b) => -a.id.localeCompare(b.id))
115 .map((p, i, arr) => GraphNode.makePortTemplate(
118 SVGUtils.matrixToTransformAttr(
119 GraphNode.createPortMatrix(arr.length, i, radius, "output")
122 .reduce((acc, tpl) => acc + tpl, "");
125 <g tabindex="-1" class="node ${nodeTypeClass} ${typeClass} ${itemsClass}"
126 data-connection-id="${dataModel.connectionId}"
127 transform="matrix(1, 0, 0, 1, ${x}, ${y})"
128 data-id="${dataModel.id}">
130 <g class="core" transform="matrix(1, 0, 0, 1, 0, 0)">
131 <circle cx="0" cy="0" r="${radius}" class="outer"></circle>
132 <circle cx="0" cy="0" r="${radius * .75}" class="inner"></circle>
134 ${GraphNode.makeIconFragment(dataModel)}
137 <text transform="matrix(${labelScale},0,0,${labelScale},0,${radius + 30})" class="title label">${HtmlUtils.escapeHTML(dataModel.label || dataModel.id)}</text>
139 ${inputPortTemplates}
140 ${outputPortTemplates}
145 private static makePortTemplate(port: {
150 type: "input" | "output",
151 transform = "matrix(1, 0, 0, 1, 0, 0)"): string {
153 const portClass = type === "input" ? "input-port" : "output-port";
154 const label = port.label || port.id;
157 <g class="port ${portClass}" transform="${transform || "matrix(1, 0, 0, 1, 0, 0)"}"
158 data-connection-id="${port.connectionId}"
159 data-port-id="${port.id}"
162 <circle cx="0" cy="0" r="7" class="port-handle"></circle>
164 <text x="0" y="0" transform="matrix(1,0,0,1,0,0)" class="label unselectable">${label}</text>
170 public static createPortMatrix(totalPortLength: number,
173 type: "input" | "output"): SVGMatrix {
174 const availableAngle = 140;
177 // Starting rotation angle
178 (-availableAngle / 2) +
180 // Angular offset by element index
182 // Angle between elements
183 * availableAngle / (totalPortLength + 1)
186 if (type === "input") {
188 // Determines the starting rotation angle
189 180 - (availableAngle / -2)
190 // Determines the angular offset modifier for the current index
192 // Determines the angular offset
193 * availableAngle / (totalPortLength + 1);
196 const matrix = SVGUtils.createMatrix();
197 return matrix.rotate(rotationAngle).translate(radius, 0).rotate(-rotationAngle);
200 static patchModelPorts<T>(model: T & { connectionId: string, id: string }): T {
201 const patch = [{connectionId: model.connectionId, isVisible: true, id: model.id}];
202 if (model instanceof WorkflowInputParameterModel) {
203 const copy = Object.create(model);
204 return Object.assign(copy, {out: patch});
207 } else if (model instanceof WorkflowOutputParameterModel) {
208 const copy = Object.create(model);
209 return Object.assign(copy, {in: patch});