6cee85d55b6bc3b16785887381a4ac94e7eea1ce
[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 this.d3_graph.nodes = args.graph_data.vertices;
122 this.d3_graph.links = args.graph_data.edges;
123 this.d3_graph.graph_parameters = args.graph_data.graph_parameters;
124 this.model = args.model;
125 this.refreshGraphParameters(this.d3_graph.graph_parameters);
126 this.refresh();
127 this.startForce();
128 //if(args.filter_base != undefined)
129
130 //if(args.filter_base){
131 var self = this;
132 setTimeout(function () {
133 self.handleForce(true);
134 self.handleFiltersParams(args.filter_base);
135 }, 500);
136 //}
137 }
138
139 /**
140 * Add a new node to the graph.
141 * @param {Object} Required. An object that specifies tha data of the new node.
142 * @returns {boolean}
143 */
144 ModelGraphEditor.prototype.addNode = function (node, success, error) {
145 var self = this;
146 var current_layer = self.getCurrentView();
147 var node_type = node.info.type;
148
149 if (self.model.layer[current_layer] && self.model.layer[current_layer].nodes[node_type] && self.model.layer[current_layer].nodes[node_type].addable) {
150 if (self.model.layer[current_layer].nodes[node_type].addable.callback) {
151 var c = self.model.callback[self.model.layer[current_layer].nodes[node_type].addable.callback].class;
152 var controller = new TCD3[c]();
153 controller[self.model.layer[current_layer].nodes[node_type].addable.callback](self, node, function () {
154 self.parent.addNode.call(self, node);
155 success && success();
156 }, error);
157
158 } else {
159
160 log('addNode: callback undefined in model spec.');
161 error && error("You can't add a " + node.info.type + ", callback undefined.");
162 }
163 } else {
164 //FIXME Error handling????
165 log("You can't add a " + node.info.type + " in a current layer " + current_layer);
166 error && error("You can't add a " + node.info.type + " in a current layer " + current_layer);
167 }
168 };
169
170
171 /**
172 * Update the data properties of the node
173 * @param {Object} Required. An object that specifies tha data of the node.
174 * @returns {boolean}
175 */
176 ModelGraphEditor.prototype.updateDataNode = function (args) {
177 //FIXME updating a node properties need commit to server side!
178 this.parent.updateDataNode.call(this, args);
179 };
180
181 /**
182 * Remove a node from graph and related links.
183 * @param {String} Required. Id of node to remove.
184 * @returns {boolean}
185 */
186 ModelGraphEditor.prototype.removeNode = function (node, success, error) {
187 console.log('removeNode', JSON.stringify(node))
188 var self = this;
189 var current_layer = self.getCurrentView();
190 var node_type = node.info.type;
191 if (node.info.desc_id == undefined) {
192 node.info.desc_id = self.desc_id;
193 }
194 if (self.model.layer[current_layer] && self.model.layer[current_layer].nodes[node_type] && self.model.layer[current_layer].nodes[node_type].removable) {
195 if (self.model.layer[current_layer].nodes[node_type].removable.callback) {
196 var c = self.model.callback[self.model.layer[current_layer].nodes[node_type].removable.callback].class;
197 var controller = new TCD3[c]();
198 controller[self.model.layer[current_layer].nodes[node_type].removable.callback](self, node, function () {
199 self.parent.removeNode.call(self, node);
200 success && success();
201 }, error);
202 } else {
203
204 log('removeNode: callback undefined in model spec.');
205 error && error("You can't remove a " + node.info.type + ", callback undefined.");
206 }
207 } else {
208 //FIXME we need to manage alert in a different way: FAILBACK
209 log("You can't remove a " + node.info.type);
210 error && error("You can't remove a " + node.info.type);
211 }
212 };
213
214 /**
215 * Add a new link to graph.
216 * @param {Object} Required. An object that specifies tha data of the new Link.
217 * @returns {boolean}
218 */
219 ModelGraphEditor.prototype.addLink = function (s, d, success, error) {
220 var self = this;
221 var source_id = s.id;
222 var target_id = d.id;
223 var source_type = s.info.type;
224 var destination_type = d.info.type;
225 var link = {
226 source: s,
227 target: d,
228 view: this.filter_parameters.link.view[0],
229 group: this.filter_parameters.link.group,
230 desc_id: this.desc_id
231 };
232 log("addLink: " + JSON.stringify(link))
233 var current_layer = self.getCurrentView()
234 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]) {
235
236 if (self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type].callback) {
237 var callback = self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type].callback;
238 console.log(callback, self.model.callback)
239 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;
240 link.directed_edge = direct_edge;
241 var c = self.model.callback[callback].class;
242 var controller = new TCD3[c]();
243 controller[callback](self, link, function () {
244 self._deselectAllNodes();
245 self.parent.addLink.call(self, link);
246 if (success)
247 success();
248 }, error);
249 } else {
250 log('addLink: callback undefined in model spec.');
251 error && error("You can't add a link, callback undefined.");
252 }
253
254 } else {
255 //FIXME we need to manage alert in a different way: FAILBACK
256 log("You can't link a " + source_type + " with a " + destination_type);
257
258 error && error("You can't link a " + source_type + " with a " + destination_type);
259 }
260 };
261
262 /**
263 * Remove a link from graph.
264 * @param {String} Required. The identifier of link to remove.
265 * @returns {boolean}
266 */
267 ModelGraphEditor.prototype.removeLink = function (link, success, error) {
268 var self = this;
269 var s = link.source;
270 var d = link.target;
271 var source_type = s.info.type;
272 var destination_type = d.info.type;
273 var current_layer = self.getCurrentView()
274 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] &&
275 self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type].removable
276 ) {
277 if (self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type].removable.callback) {
278 var callback = self.model.layer[current_layer].allowed_edges[source_type].destination[destination_type].removable.callback;
279 var c = self.model.callback[callback].class;
280 var controller = new TCD3[c]();
281 controller[callback](self, link, function () {
282 self._deselectAllNodes();
283 self._deselectAllLinks();
284 self.parent.removeLink.call(self, link.index);
285 success && success();
286 }, error);
287 } else {
288 log('removeLink: callback undefined in model spec.');
289 error && error("You can't remove a link, callback undefined.");
290 }
291
292 } else {
293 //FIXME we need to manage alert in a different way: FAILBACK
294 log("You can't delete the link");
295 error && error("You can't delete the link");
296 }
297
298
299 };
300
301
302 ModelGraphEditor.prototype.savePositions = function (data) {
303 var vertices = {};
304 this.node.each(function (d) {
305 vertices[d.id] = {};
306 vertices[d.id]['x'] = d.x;
307 vertices[d.id]['y'] = d.y;
308 });
309 new TCD3.GraphRequests().savePositions({
310 'vertices': vertices
311 });
312
313 };
314
315 /**
316 * Internal functions
317 */
318
319 ModelGraphEditor.prototype._setupBehaviorsOnEvents = function (layer) {
320
321 var self = this;
322 var contextMenuLinksAction = [{
323 title: 'Delete Link',
324 action: function (elm, link, i) {
325 self.removeLink(link, null, showAlert);
326 },
327 edit_mode: true
328 }];
329 var contextMenuNodesAction = [{
330 title: 'Edit',
331 action: function (elm, d, i) {
332 if (d.info.type != undefined) {
333 self.eventHandler.fire("edit_descriptor", self.project_id, d);
334 }
335 },
336 nodes: [],
337 edit_mode: true
338 },
339 {
340 title: 'Delete',
341 action: function (elm, d, i) {
342 self.removeNode(d, null, showAlert);
343 },
344 edit_mode: true
345 }
346
347 ];
348 if (this.customBehavioursOnEvents) {
349 contextMenuNodesAction = contextMenuNodesAction.concat(this.customBehavioursOnEvents['behaviors'].nodes);
350 }
351
352
353 if (self.model && self.model.layer && self.model.layer[layer] && self.model.layer[layer].action && self.model.layer[layer].action.node) {
354 for (var i in self.model.layer[layer].action.node) {
355 var action = self.model.layer[layer].action.node[i]
356 contextMenuNodesAction.push({
357 title: action.title,
358 action: function (elm, d, i) {
359 var callback = action.callback;
360 var c = self.model.callback[callback].class;
361 var controller = new TCD3[c]();
362 var args = {
363 elm: elm,
364 d: d,
365 i: i
366 };
367
368 controller[callback](self, args);
369 },
370 edit_mode: (action.edit_mode !== undefined) ? action.edit_mode : undefined
371 });
372 }
373 }
374
375 this.behavioursOnEvents = {
376 'nodes': {
377 'click': function (d) {
378
379 d3.event.preventDefault();
380
381 if (self._edit_mode && self.lastKeyDown === SHIFT_BUTTON && self._selected_node !== undefined) {
382 self.addLink(self._selected_node, d, null, showAlert);
383 } else {
384 self._selectNodeExclusive(this, d);
385 }
386
387 },
388 'mouseover': function (d) {
389 self.link.style('stroke-width', function (l) {
390 if (d === l.source || d === l.target)
391 return 4;
392 else
393 return 2;
394 });
395 },
396 'mouseout': function (d) {
397 self.link.style('stroke-width', 2);
398 },
399 'contextmenu': d3.contextMenu(contextMenuNodesAction, {
400 'edit_mode': self._edit_mode,
401 'layer': layer,
402 'type_object': 'node'
403 })
404 },
405 'links': {
406 'click': function (d) {
407 self._selectLinkExclusive(this, d);
408
409 },
410 'dblclick': function (event) {
411
412 },
413 'mouseover': function (d) {
414 d3.select(this).style('stroke-width', 4);
415 },
416 'mouseout': function (d) {
417 if (d !== self._selected_link)
418 d3.select(this).style('stroke-width', 2);
419 },
420 'contextmenu': d3.contextMenu(contextMenuLinksAction, {
421 'edit_mode': self._edit_mode,
422 'layer': layer,
423 'type_object': 'link'
424 })
425 }
426 }
427 };
428
429 ModelGraphEditor.prototype.handleFiltersParams = function (filtersParams, notFireEvent) {
430
431 this.parent.handleFiltersParams.call(this, filtersParams, notFireEvent);
432 if (filtersParams && filtersParams.link && filtersParams.link.view)
433 this._setupBehaviorsOnEvents(filtersParams.link.view[0]);
434 };
435
436 ModelGraphEditor.prototype.getAvailableNodes = function () {
437 log('getAvailableNodes');
438 log(this.model);
439 if (this.model && this.model.layer[this.getCurrentView()] !== undefined)
440 return this.model.layer[this.getCurrentView()].nodes;
441 return [];
442 };
443
444
445 ModelGraphEditor.prototype.getTypeProperty = function () {
446 return this.type_property;
447 };
448
449 ModelGraphEditor.prototype.getCurrentGroup = function () {
450 return this.filter_parameters.node.group[0];
451
452 };
453
454 ModelGraphEditor.prototype.getCurrentView = function () {
455 return this.filter_parameters.link.view[0];
456 };
457 ModelGraphEditor.prototype.getCurrentFilters = function () {
458 return this.filter_parameters;
459 };
460
461 ModelGraphEditor.prototype.getGraphParams = function () {
462 return this.d3_graph.graph_parameters;
463 };
464
465 /**
466 * Log utility
467 */
468 function log(text) {
469 if (DEBUG)
470 console.log("::ModelGraphEditor::", text);
471 }
472
473
474 return ModelGraphEditor;
475
476
477 }(this));
478
479 if (typeof module === 'object') {
480 module.exports = TCD3.ModelGraphEditor;
481 }