[wip] NS instance topology view
[osm/LW-UI.git] / static / TopologyComposer / js / model_graph_editor.js
1 /*
2 Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni
3 Copyright 2018 EveryUP srl
4
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
16 */
17 if (typeof TCD3 === 'undefined') {
18 var TCD3 = {};
19 }
20
21 TCD3.ModelGraphEditor = (function () {
22 'use strict';
23
24 var DEBUG = true;
25 var SHIFT_BUTTON = 16;
26 var IMAGE_PATH = "/static/assets/img/";
27
28
29 ModelGraphEditor.prototype = new TCD3.GraphEditor();
30 ModelGraphEditor.prototype.constructor = ModelGraphEditor;
31 ModelGraphEditor.prototype.parent = TCD3.GraphEditor.prototype;
32
33 /**
34 * Constructor
35 */
36 function ModelGraphEditor(args) {
37
38 log("Constructor");
39
40 }
41
42
43 ModelGraphEditor.prototype.init = function (args) {
44 this.parent.init.call(this, args);
45 var self = this;
46 this.desc_id = args.desc_id || undefined; //TODO remove it
47
48 this.type_property = {};
49 this.type_property["unrecognized"] = args.gui_properties["nodes"]["default"];
50
51 this._edit_mode = args.edit_mode || false;
52
53 Object.keys(args.gui_properties["nodes"]).forEach(function (key, index) {
54
55 this.type_property[key] = args.gui_properties["nodes"][key];
56
57 this.type_property[key]["shape"] = this.parent.get_d3_symbol(this.type_property[key]["shape"]);
58 if (this.type_property[key]["image"] !== undefined) {
59 this.type_property[key]["image"] = IMAGE_PATH + this.type_property[key]["image"];
60 }
61
62 }, this);
63
64 if (args.gui_properties["edges"]) {
65 this.type_property_link = args.gui_properties["edges"];
66 var link_types = ['unrecognized'].concat(Object.keys(self.type_property_link))
67 this.defs.selectAll("marker")
68 .data(link_types)
69 .enter()
70 .append("svg:marker") // This section adds in the arrows
71 .attr("id", function (d) {
72 return d;
73 })
74 .attr("viewBox", "-5 -5 10 10")
75 .attr("refX", 13) /*must be smarter way to calculate shift*/
76 .attr("refY", 0)
77 .attr("markerUnits", "userSpaceOnUse")
78 .attr("markerWidth", 12)
79 .attr("markerHeight", 12)
80 .attr("orient", "auto")
81 .append("path")
82 .attr("d", "M 0,0 m -5,-5 L 5,0 L -5,5 Z")
83 .attr('fill', function (d) {
84 return self.type_property_link[d].color;
85 });
86 }
87
88 this.customBehavioursOnEvents = args.behaviorsOnEvents || undefined;
89
90
91 var data_url = args.data_url || undefined;
92 if (!args.graph_data && args.data_url) {
93 d3.json(data_url, function (error, data) {
94 //console.log(JSON.stringify(data))
95 self.d3_graph.nodes = data.vertices;
96 self.d3_graph.links = data.edges;
97 self.d3_graph.graph_parameters = data.graph_parameters;
98 self.model = data.model;
99 self.refreshGraphParameters(self.d3_graph.graph_parameters);
100 self.refresh();
101 self.startForce();
102 //if(args.filter_base != undefined)
103
104 setTimeout(function () {
105 self.handleForce(true);
106 self.handleFiltersParams(args.filter_base);
107 }, 500);
108
109 });
110 } else {
111 this.updateData(args)
112 }
113 }
114
115 /**
116 * Update data of the graph.
117 * @param {Object} Required. An object that specifies tha data of the new node.
118 * @returns {boolean}
119 */
120 ModelGraphEditor.prototype.updateData = function (args) {
121 console.log("updateData")
122 this.d3_graph.nodes = args.graph_data.vertices;
123 this.d3_graph.links = args.graph_data.edges;
124 this.d3_graph.graph_parameters = args.graph_parameters;
125 this.model = args.model;
126 this.refreshGraphParameters(this.d3_graph.graph_parameters);
127 this.refresh();
128 this.startForce();
129 //if(args.filter_base != undefined)
130
131 //if(args.filter_base){
132 var self = this;
133 setTimeout(function () {
134 self.handleForce(true);
135 self.handleFiltersParams(args.filter_base);
136 }, 500);
137 //}
138 }
139
140 /**
141 * Add a new node to the graph.
142 * @param {Object} Required. An object that specifies tha data of the new node.
143 * @returns {boolean}
144 */
145 ModelGraphEditor.prototype.addNode = function (node, success, error) {
146 var self = this;
147 var current_layer = self.getCurrentView();
148 var node_type = node.info.type;
149
150 if (self.model.layer[current_layer] && self.model.layer[current_layer].nodes[node_type] && self.model.layer[current_layer].nodes[node_type].addable) {
151 if (self.model.layer[current_layer].nodes[node_type].addable.callback) {
152 var c = self.model.callback[self.model.layer[current_layer].nodes[node_type].addable.callback].class;
153 var controller = new TCD3[c]();
154 controller[self.model.layer[current_layer].nodes[node_type].addable.callback](self, node, function () {
155 self.parent.addNode.call(self, node);
156 success && success();
157 }, error);
158
159 } else {
160
161 log('addNode: callback undefined in model spec.');
162 error && error("You can't add a " + node.info.type + ", callback undefined.");
163 }
164 } else {
165 //FIXME Error handling????
166 log("You can't add a " + node.info.type + " in a current layer " + current_layer);
167 error && error("You can't add a " + node.info.type + " in a current layer " + current_layer);
168 }
169 };
170
171
172 /**
173 * Update the data properties of the node
174 * @param {Object} Required. An object that specifies tha data of the node.
175 * @returns {boolean}
176 */
177 ModelGraphEditor.prototype.updateDataNode = function (args) {
178 //FIXME updating a node properties need commit to server side!
179 this.parent.updateDataNode.call(this, args);
180 };
181
182 /**
183 * Remove a node from graph and related links.
184 * @param {String} Required. Id of node to remove.
185 * @returns {boolean}
186 */
187 ModelGraphEditor.prototype.removeNode = function (node, success, error) {
188 console.log('removeNode', JSON.stringify(node))
189 var self = this;
190 var current_layer = self.getCurrentView();
191 var node_type = node.info.type;
192 if (node.info.desc_id == undefined) {
193 node.info.desc_id = self.desc_id;
194 }
195 if (self.model.layer[current_layer] && self.model.layer[current_layer].nodes[node_type] && self.model.layer[current_layer].nodes[node_type].removable) {
196 if (self.model.layer[current_layer].nodes[node_type].removable.callback) {
197 var c = self.model.callback[self.model.layer[current_layer].nodes[node_type].removable.callback].class;
198 var controller = new TCD3[c]();
199 controller[self.model.layer[current_layer].nodes[node_type].removable.callback](self, node, function () {
200 self.parent.removeNode.call(self, node);
201 success && success();
202 }, error);
203 } else {
204
205 log('removeNode: callback undefined in model spec.');
206 error && error("You can't remove a " + node.info.type + ", callback undefined.");
207 }
208 } else {
209 //FIXME we need to manage alert in a different way: FAILBACK
210 log("You can't remove a " + node.info.type);
211 error && error("You can't remove a " + node.info.type);
212 }
213 };
214
215 /**
216 * Add a new link to graph.
217 * @param {Object} Required. An object that specifies tha data of the new Link.
218 * @returns {boolean}
219 */
220 ModelGraphEditor.prototype.addLink = function (s, d, success, error) {
221 var self = this;
222 var source_id = s.id;
223 var target_id = d.id;
224 var source_type = s.info.type;
225 var destination_type = d.info.type;
226 var link = {
227 source: s,
228 target: d,
229 view: this.filter_parameters.link.view[0],
230 group: this.filter_parameters.link.group,
231 desc_id: this.desc_id
232 };
233 log("addLink: " + JSON.stringify(link))
234 var current_layer = self.getCurrentView()
235 if (self.model.layer[current_layer].allowed_edges && self.model.layer[current_layer].allowed_edges[source_type] && self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type]) {
236
237 if (self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type].callback) {
238 var callback = self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type].callback;
239 console.log(callback, self.model.callback)
240 var direct_edge = 'direct_edge' in self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type] ? self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type]['direct_edge'] : false;
241 link.directed_edge = direct_edge;
242 var c = self.model.callback[callback].class;
243 var controller = new TCD3[c]();
244 controller[callback](self, link, function () {
245 self._deselectAllNodes();
246 self.parent.addLink.call(self, link);
247 if (success)
248 success();
249 }, error);
250 } else {
251 log('addLink: callback undefined in model spec.');
252 error && error("You can't add a link, callback undefined.");
253 }
254
255 } else {
256 //FIXME we need to manage alert in a different way: FAILBACK
257 log("You can't link a " + source_type + " with a " + destination_type);
258
259 error && error("You can't link a " + source_type + " with a " + destination_type);
260 }
261 };
262
263 /**
264 * Remove a link from graph.
265 * @param {String} Required. The identifier of link to remove.
266 * @returns {boolean}
267 */
268 ModelGraphEditor.prototype.removeLink = function (link, success, error) {
269 var self = this;
270 var s = link.source;
271 var d = link.target;
272 var source_type = s.info.type;
273 var destination_type = d.info.type;
274 var current_layer = self.getCurrentView()
275 if (self.model.layer[current_layer].allowed_edges && self.model.layer[current_layer].allowed_edges[source_type] && self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type] &&
276 self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type].removable
277 ) {
278 if (self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type].removable.callback) {
279 var callback = self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type].removable.callback;
280 var c = self.model.callback[callback].class;
281 var controller = new TCD3[c]();
282 controller[callback](self, link, function () {
283 self._deselectAllNodes();
284 self._deselectAllLinks();
285 self.parent.removeLink.call(self, link.index);
286 success && success();
287 }, error);
288 } else {
289 log('removeLink: callback undefined in model spec.');
290 error && error("You can't remove a link, callback undefined.");
291 }
292
293 } else {
294 //FIXME we need to manage alert in a different way: FAILBACK
295 log("You can't delete the link");
296 error && error("You can't delete the link");
297 }
298
299
300 };
301
302
303 ModelGraphEditor.prototype.savePositions = function (data) {
304 var vertices = {};
305 this.node.each(function (d) {
306 vertices[d.id] = {};
307 vertices[d.id]['x'] = d.x;
308 vertices[d.id]['y'] = d.y;
309 });
310 new TCD3.GraphRequests().savePositions({
311 'vertices': vertices
312 });
313
314 };
315
316 /**
317 * Internal functions
318 */
319
320 ModelGraphEditor.prototype._setupBehaviorsOnEvents = function (layer) {
321
322 var self = this;
323 var contextMenuLinksAction = [{
324 title: 'Delete Link',
325 action: function (elm, link, i) {
326 self.removeLink(link, null, showAlert);
327 },
328 edit_mode: true
329 }];
330 var contextMenuNodesAction = [{
331 title: 'Edit',
332 action: function (elm, d, i) {
333 if (d.info.type != undefined) {
334 self.eventHandler.fire("edit_descriptor", self.project_id, d);
335 }
336 },
337 nodes: [],
338 edit_mode: true
339 },
340 {
341 title: 'Delete',
342 action: function (elm, d, i) {
343 self.removeNode(d, null, showAlert);
344 },
345 edit_mode: true
346 }
347
348 ];
349 if (this.customBehavioursOnEvents) {
350 contextMenuNodesAction = contextMenuNodesAction.concat(this.customBehavioursOnEvents['behaviors'].nodes);
351 }
352
353
354 if (self.model && self.model.layer && self.model.layer[layer] && self.model.layer[layer].action && self.model.layer[layer].action.node) {
355 for (var i in self.model.layer[layer].action.node) {
356 var action = self.model.layer[layer].action.node[i]
357 contextMenuNodesAction.push({
358 title: action.title,
359 action: function (elm, d, i) {
360 var callback = action.callback;
361 var c = self.model.callback[callback].class;
362 var controller = new TCD3[c]();
363 var args = {
364 elm: elm,
365 d: d,
366 i: i
367 };
368
369 controller[callback](self, args);
370 },
371 edit_mode: (action.edit_mode !== undefined) ? action.edit_mode : undefined
372 });
373 }
374 }
375
376 this.behavioursOnEvents = {
377 'nodes': {
378 'click': function (d) {
379
380 d3.event.preventDefault();
381
382 if (self._edit_mode && self.lastKeyDown === SHIFT_BUTTON && self._selected_node !== undefined) {
383 self.addLink(self._selected_node, d, null, showAlert);
384 } else {
385 self._selectNodeExclusive(this, d);
386 }
387
388 },
389 'mouseover': function (d) {
390 self.link.style('stroke-width', function (l) {
391 if (d === l.source || d === l.target)
392 return 4;
393 else
394 return 2;
395 });
396 },
397 'mouseout': function (d) {
398 self.link.style('stroke-width', 2);
399 },
400 'contextmenu': d3.contextMenu(contextMenuNodesAction, {
401 'edit_mode': self._edit_mode,
402 'layer': layer,
403 'type_object': 'node'
404 })
405 },
406 'links': {
407 'click': function (d) {
408 self._selectLinkExclusive(this, d);
409
410 },
411 'dblclick': function (event) {
412
413 },
414 'mouseover': function (d) {
415 d3.select(this).style('stroke-width', 4);
416 },
417 'mouseout': function (d) {
418 if (d !== self._selected_link)
419 d3.select(this).style('stroke-width', 2);
420 },
421 'contextmenu': d3.contextMenu(contextMenuLinksAction, {
422 'edit_mode': self._edit_mode,
423 'layer': layer,
424 'type_object': 'link'
425 })
426 }
427 }
428 };
429
430 ModelGraphEditor.prototype.handleFiltersParams = function (filtersParams, notFireEvent) {
431
432 this.parent.handleFiltersParams.call(this, filtersParams, notFireEvent);
433 if (filtersParams && filtersParams.link && filtersParams.link.view)
434 this._setupBehaviorsOnEvents(filtersParams.link.view[0]);
435 };
436
437 ModelGraphEditor.prototype.getAvailableNodes = function () {
438 log('getAvailableNodes');
439 log(this.model);
440 if (this.model && this.model.layer[this.getCurrentView()] !== undefined)
441 return this.model.layer[this.getCurrentView()].nodes;
442 return [];
443 };
444
445
446 ModelGraphEditor.prototype.getTypeProperty = function () {
447 return this.type_property;
448 };
449
450 ModelGraphEditor.prototype.getCurrentGroup = function () {
451 return this.filter_parameters.node.group[0];
452
453 };
454
455 ModelGraphEditor.prototype.getCurrentView = function () {
456 return this.filter_parameters.link.view[0];
457 };
458 ModelGraphEditor.prototype.getCurrentFilters = function () {
459 return this.filter_parameters;
460 };
461
462 ModelGraphEditor.prototype.getGraphParams = function () {
463 return this.d3_graph.graph_parameters;
464 };
465
466 /**
467 * Log utility
468 */
469 function log(text) {
470 if (DEBUG)
471 console.log("::ModelGraphEditor::", text);
472 }
473
474
475 return ModelGraphEditor;
476
477
478 }(this));
479
480 if (typeof module === 'object') {
481 module.exports = TCD3.ModelGraphEditor;
482 }