15781: Uses 'contains' filter operator to search for properties.
[arvados.git] / src / lib / cwl-svg / graph / graph-node.ts
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";
5
6 export type NodePosition = { x: number, y: number };
7 export type NodeDataModel = WorkflowInputParameterModel | WorkflowOutputParameterModel | StepModel;
8
9 export class GraphNode {
10
11     public position: NodePosition = {x: 0, y: 0};
12
13     static radius = 30;
14
15     constructor(position: Partial<NodePosition>,
16                 private dataModel: NodeDataModel) {
17
18         this.dataModel = dataModel;
19
20         Object.assign(this.position, position);
21     }
22
23     /**
24      * @FIXME Making icons increases the rendering time by 50-100%. Try embedding the SVG directly.
25      */
26
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>";
33
34     private static makeIconFragment(model: any) {
35
36         let iconStr = "";
37
38         if (model instanceof StepModel && model.run) {
39
40             if (model.run.class === "Workflow") {
41                 iconStr = this.workflowIconSvg;
42             } else if (model.run.class === "CommandLineTool") {
43                 iconStr = this.toolIconSvg;
44             }
45
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;
49             } else {
50                 iconStr = this.inputIconSvg;
51             }
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;
55             } else {
56                 iconStr = this.outputIconSvg;
57             }
58         }
59
60         return iconStr;
61     }
62
63     static makeTemplate(dataModel: {
64         id: string,
65         connectionId: string,
66         label?: string,
67         in?: any[],
68         type?: ParameterTypeModel
69         out?: any[],
70         customProps?: {
71             "sbg:x"?: number
72             "sbg:y"?: number
73         }
74     }, labelScale = 1): string {
75
76         const x = ~~(dataModel.customProps && dataModel.customProps["sbg:x"])!;
77         const y = ~~(dataModel.customProps && dataModel.customProps["sbg:y"])!;
78
79         let nodeTypeClass = "step";
80         if (dataModel instanceof WorkflowInputParameterModel) {
81             nodeTypeClass = "input";
82         } else if (dataModel instanceof WorkflowOutputParameterModel) {
83             nodeTypeClass = "output";
84         }
85
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;
90
91         let typeClass = "";
92         let itemsClass = "";
93
94         if (dataModel.type) {
95             typeClass = "type-" + dataModel.type.type;
96
97             if(dataModel.type.items){
98                 itemsClass = "items-" + dataModel.type.items;
99             }
100         }
101
102         const inputPortTemplates = inputs
103             .sort((a, b) => -a.id.localeCompare(b.id))
104             .map((p, i, arr) => GraphNode.makePortTemplate(
105                 p,
106                 "input",
107                 SVGUtils.matrixToTransformAttr(
108                     GraphNode.createPortMatrix(arr.length, i, radius, "input")
109                 )
110             ))
111             .reduce((acc, tpl) => acc + tpl, "");
112
113         const outputPortTemplates = outputs
114             .sort((a, b) => -a.id.localeCompare(b.id))
115             .map((p, i, arr) => GraphNode.makePortTemplate(
116                 p,
117                 "output",
118                 SVGUtils.matrixToTransformAttr(
119                     GraphNode.createPortMatrix(arr.length, i, radius, "output")
120                 )
121             ))
122             .reduce((acc, tpl) => acc + tpl, "");
123
124         return `
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}">
129                
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>
133                     
134                     ${GraphNode.makeIconFragment(dataModel)}
135                 </g>
136                 
137                 <text transform="matrix(${labelScale},0,0,${labelScale},0,${radius + 30})" class="title label">${HtmlUtils.escapeHTML(dataModel.label || dataModel.id)}</text>
138                 
139                 ${inputPortTemplates}
140                 ${outputPortTemplates}
141             </g>
142         `;
143     }
144
145     private static makePortTemplate(port: {
146                                         label?: string,
147                                         id: string,
148                                         connectionId: string
149                                     },
150                                     type: "input" | "output",
151                                     transform = "matrix(1, 0, 0, 1, 0, 0)"): string {
152
153         const portClass = type === "input" ? "input-port" : "output-port";
154         const label     = port.label || port.id;
155
156         return `
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}"
160             >
161                 <g class="io-port">
162                     <circle cx="0" cy="0" r="7" class="port-handle"></circle>
163                 </g>
164                 <text x="0" y="0" transform="matrix(1,0,0,1,0,0)" class="label unselectable">${label}</text>
165             </g>
166             
167         `;
168     }
169
170     public static createPortMatrix(totalPortLength: number,
171                                    portIndex: number,
172                                    radius: number,
173                                    type: "input" | "output"): SVGMatrix {
174         const availableAngle = 140;
175
176         let rotationAngle =
177                 // Starting rotation angle
178                 (-availableAngle / 2) +
179                 (
180                     // Angular offset by element index
181                     (portIndex + 1)
182                     // Angle between elements
183                     * availableAngle / (totalPortLength + 1)
184                 );
185
186         if (type === "input") {
187             rotationAngle =
188                 // Determines the starting rotation angle
189                 180 - (availableAngle / -2)
190                 // Determines the angular offset modifier for the current index
191                 - (portIndex + 1)
192                 // Determines the angular offset
193                 * availableAngle / (totalPortLength + 1);
194         }
195
196         const matrix = SVGUtils.createMatrix();
197         return matrix.rotate(rotationAngle).translate(radius, 0).rotate(-rotationAngle);
198     }
199
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});
205
206
207         } else if (model instanceof WorkflowOutputParameterModel) {
208             const copy = Object.create(model);
209             return Object.assign(copy, {in: patch});
210         }
211
212         return model;
213     }
214
215 }