blob: 5731019ff0afde7e42b162eda4f493c09c1c4a82 [file] [log] [blame]
Michael Landoed64b5e2017-06-09 03:19:04 +03001(function (scope) {
2 var Class = function (param1, param2) {
3
4 var extend, mixins, definition;
5 if (param2) { //two parameters passed, first is extends, second definition object
6 extend = Array.isArray(param1) ? param1[0] : param1;
7 mixins = Array.isArray(param1) ? param1.slice(1) : null;
8 definition = param2;
9 } else { //only one parameter passed => no extend, only definition
10 extend = null;
11 definition = param1;
12 }
13
14
15 var Definition = definition.hasOwnProperty("constructor") ? definition.constructor : function () {
16 };
17
18 Definition.prototype = Object.create(extend ? extend.prototype : null);
19 var propertiesObject = definition.propertiesObject ? definition.propertiesObject : {};
20 if (mixins) {
21 var i, i2;
22 for (i in mixins) {
23 for (i2 in mixins[i].prototype) {
24 Definition.prototype[i2] = mixins[i].prototype[i2];
25 }
26 for (var i2 in mixins[i].prototype.propertiesObject) {
27 propertiesObject[i2] = mixins[i].prototype.propertiesObject[i2];
28 }
29 }
30 }
31
32 Definition.prototype.propertiesObject = propertiesObject;
33
34 Object.defineProperties(Definition.prototype, propertiesObject);
35
36 for (var key in definition) {
37 if (definition.hasOwnProperty(key)) {
38 Definition.prototype[key] = definition[key];
39 }
40 }
41
42 Definition.prototype.constructor = Definition;
43
44 return Definition;
45 };
46
47
48 var Interface = function (properties) {
49 this.properties = properties;
50 };
51
52 var InterfaceException = function (message) {
53 this.name = "InterfaceException";
54 this.message = message || "";
55 };
56
57 InterfaceException.prototype = new Error();
58
59 Interface.prototype.implements = function (target) {
60 for (var i in this.properties) {
61 if (target[this.properties[i]] == undefined) {
62 throw new InterfaceException("Missing property " + this.properties[i]);
63 }
64 }
65 return true;
66 };
67
68 Interface.prototype.doesImplement = function (target) {
69 for (var i in this.properties) {
70 if (target[this.properties[i]] === undefined) {
71 return false;
72 }
73 }
74 return true;
75 };
76
77 var VectorMath = {
78 distance: function (vector1, vector2) {
79 return Math.sqrt(Math.pow(vector1.x - vector2.x, 2) + Math.pow(vector1.y - vector2.y, 2));
80 }
81 };
82
83 var EventDispatcher = Class({
84 constructor: function () {
85 this.events = {};
86 },
87 on: function (name, listener, context) {
88 this.events[name] = this.events[name] ? this.events[name] : [];
89 this.events[name].push({
90 listener: listener,
91 context: context
92 })
93 },
94 once: function (name, listener, context) {
95 this.off(name, listener, context);
96 this.on(name, listener, context);
97 },
98 off: function (name, listener, context) {
99 //no event with this name registered? => finish
100 if (!this.events[name]) {
101 return;
102 }
103 if (listener) { //searching only for certains listeners
104 for (var i in this.events[name]) {
105 if (this.events[name][i].listener === listener) {
106 if (!context || this.events[name][i].context === context) {
107 this.events[name].splice(i, 1);
108 }
109 }
110 }
111 } else {
112 delete this.events[name];
113 }
114 },
115 trigger: function (name) {
116 var listeners = this.events[name];
117
118 for (var i in listeners) {
119 listeners[i].listener.apply(listeners[i].context, Array.prototype.slice.call(arguments, 1));
120 }
121 }
122 });
123
124 exports.CytoscapeEdgeEditation = Class({
125
126 init: function (cy, handleSize) {
127 this.DOUBLE_CLICK_INTERVAL = 300;
128 this.HANDLE_SIZE = handleSize ? handleSize : 5;
129 this.ARROW_END_ID = "ARROW_END_ID";
130
131 this._handles = {};
132 this._dragging = false;
133 this._hover = null;
134
135
136 this._cy = cy;
137 this._$container = $(cy.container());
138
139 this._cy.on('mouseover tap', 'node', this._mouseOver.bind(this));
140 this._cy.on('mouseout', 'node', this._mouseOut.bind(this));
141
142 this._$container.on('mouseout', function (e) {
143 if (this.permanentHandle) {
144 return;
145 }
146
147 this._clear();
148 }.bind(this));
149
150 this._$container.on('mouseover', function (e) {
151 if (this._hover) {
152 this._mouseOver({cyTarget: this._hover});
153 }
154 }.bind(this));
155
156 this._cy.on("select", "node", this._redraw.bind(this))
157
158 this._cy.on("mousedown", "node", function () {
159 this._nodeClicked = true;
160 }.bind(this));
161
162 this._cy.on("mouseup", "node", function () {
163 this._nodeClicked = false;
164 }.bind(this));
165
166 this._cy.on("remove", "node", function () {
167 this._hover = false;
168 this._clear();
169 }.bind(this));
170
171 this._cy.on('showhandle', function (cy, target) {
172 this.permanentHandle = true;
173 this._showHandles(target);
174 }.bind(this));
175
176 this._cy.on('hidehandles', this._hideHandles.bind(this));
177
178 this._cy.bind('zoom pan', this._redraw.bind(this));
179
180
181 this._$canvas = $('<canvas></canvas>');
182 this._$canvas.css("top", 0);
183 this._$canvas.on("mousedown", this._mouseDown.bind(this));
184 this._$canvas.on("mousemove", this._mouseMove.bind(this));
185
186 this._ctx = this._$canvas[0].getContext('2d');
187 this._$container.children("div").append(this._$canvas);
188
189 $(window).bind('mouseup', this._mouseUp.bind(this));
190
191 /*$(window).bind('resize', this._resizeCanvas.bind(this));
192 $(window).bind('resize', this._resizeCanvas.bind(this));*/
193
194 this._cy.on("resize", this._resizeCanvas.bind(this));
195
196 this._$container.bind('resize', function () {
197 this._resizeCanvas();
198 }.bind(this));
199
200 this._resizeCanvas();
201
Michael Landoed64b5e2017-06-09 03:19:04 +0300202
Michael Landoed64b5e2017-06-09 03:19:04 +0300203
204 },
205 registerHandle: function (handle) {
206 if (handle.nodeTypeNames) {
207 for (var i in handle.nodeTypeNames) {
208 var nodeTypeName = handle.nodeTypeNames[i];
209 this._handles[nodeTypeName] = this._handles[nodeTypeName] || [];
210 this._handles[nodeTypeName].push(handle);
211 }
212 } else {
213 this._handles["*"] = this._handles["*"] || [];
214 this._handles["*"].push(handle);
215 }
216
217 },
218 _showHandles: function (target) {
219 var nodeTypeName = target.data().type;
220 if (nodeTypeName) {
221
222 var handles = this._handles[nodeTypeName] ? this._handles[nodeTypeName] : this._handles["*"];
223
224 for (var i in handles) {
225 if (handles[i].type != null) {
226 this._drawHandle(handles[i], target);
227 }
228 }
229 }
230
231 },
232 _clear: function () {
233
234 var w = this._$container.width();
235 var h = this._$container.height();
236 this._ctx.clearRect(0, 0, w, h);
237 },
238 _drawHandle: function (handle, target) {
239
240 var position = this._getHandlePosition(handle, target);
Michael Lando9db40522017-07-22 17:10:02 +0300241 var handleSize = this.HANDLE_SIZE * this._cy.zoom();
Michael Lando39a4e0c2017-07-18 20:46:42 +0300242
Michael Landoed64b5e2017-06-09 03:19:04 +0300243 this._ctx.beginPath();
244
245 if (handle.imageUrl) {
246 var base_image = new Image();
247 base_image.src = handle.imageUrl;
Michael Lando39a4e0c2017-07-18 20:46:42 +0300248 this._ctx.drawImage(base_image, position.x, position.y, handleSize, handleSize);
Michael Landoed64b5e2017-06-09 03:19:04 +0300249 } else {
250 this._ctx.arc(position.x, position.y, this.HANDLE_SIZE, 0, 2 * Math.PI, false);
251 this._ctx.fillStyle = handle.color;
252 this._ctx.strokeStyle = "white";
253 this._ctx.lineWidth = 0;
254 this._ctx.fill();
255 this._ctx.stroke();
256 }
257
258 },
259 _drawArrow: function (fromNode, toPosition, handle) {
260 var toNode;
261 if (this._hover) {
262 toNode = this._hover;
263 } else {
Michael Lando9db40522017-07-22 17:10:02 +0300264 if (!this._arrowEnd) {
265 this._arrowEnd = this._cy.add({
266 group: "nodes",
267 data: {
268 "id": this.ARROW_END_ID,
269 "position": { x: 150, y: 150 }
270 }
271 });
272
273 this._arrowEnd.css({
274 "opacity": 0,
275 'width': 0.0001,
276 'height': 0.0001
277 });
278 }
279
Michael Landoed64b5e2017-06-09 03:19:04 +0300280 this._arrowEnd.renderedPosition(toPosition);
281 toNode = this._arrowEnd;
282 }
283
284
285 if (this._edge) {
286 this._edge.remove();
287 }
288
289 this._edge = this._cy.add({
290 group: "edges",
291 data: {
292 id: "edge",
293 source: fromNode.id(),
294 target: toNode.id(),
295 type: 'temporary-link'
296 },
297 css: $.extend(
298 this._getEdgeCSSByHandle(handle),
299 {opacity: 0.5}
300 )
301 });
302
303 },
304 _clearArrow: function () {
305 if (this._edge) {
306 this._edge.remove();
307 this._edge = null;
308 }
Michael Lando9db40522017-07-22 17:10:02 +0300309
310 if (this._arrowEnd) {
311 this._arrowEnd.remove();
312 this._arrowEnd = null;
313 }
Michael Landoed64b5e2017-06-09 03:19:04 +0300314 },
315 _resizeCanvas: function () {
316 this._$canvas
317 .attr('height', this._$container.height())
318 .attr('width', this._$container.width())
319 .css({
320 'position': 'absolute',
321 'z-index': '999'
322 });
323 },
324 _mouseDown: function (e) {
325 this._hit = this._hitTestHandles(e);
326
327 if (this._hit) {
328 this._lastClick = Date.now();
329 this._dragging = this._hover;
330 this._hover = null;
331 e.stopImmediatePropagation();
332 }
333 },
334 _hideHandles: function () {
335 this.permanentHandle = false;
336 this._clear();
337
338 if(this._hover){
339 this._showHandles(this._hover);
340 }
341 },
342 _mouseUp: function () {
343 if (this._hover) {
344 if (this._hit) {
345 //check if custom listener was passed, if so trigger it and do not add edge
346 var listeners = this._cy._private.listeners;
347 for (var i = 0; i < listeners.length; i++) {
348 if (listeners[i].type === 'addedgemouseup') {
349 this._cy.trigger('addedgemouseup', {
350 source: this._dragging,
351 target: this._hover,
352 edge: this._edge
353 });
354 var that = this;
355 setTimeout(function () {
356 that._dragging = false;
357 that._clearArrow();
358 that._hit = null;
359 }, 0);
360
361
362 return;
363 }
364 }
365
366 var edgeToRemove = this._checkSingleEdge(this._hit.handle, this._dragging);
367 if (edgeToRemove) {
368 this._cy.remove("#" + edgeToRemove.id());
369 }
370 var edge = this._cy.add({
371 data: {
372 source: this._dragging.id(),
373 target: this._hover.id(),
374 type: this._hit.handle.type
375 }
376 });
377 this._initEdgeEvents(edge);
378 }
379 }
380 this._cy.trigger('handlemouseout', {
381 node: this._hover
382 });
383 $("body").css("cursor", "inherit");
384 this._dragging = false;
385 this._clearArrow();
386 },
387 _mouseMove: function (e) {
388 if (this._hover) {
389 if (!this._dragging) {
390 var hit = this._hitTestHandles(e);
391 if (hit) {
392 this._cy.trigger('handlemouseover', {
393 node: this._hover
394 });
395 $("body").css("cursor", "pointer");
396
397 } else {
398 this._cy.trigger('handlemouseout', {
399 node: this._hover
400 });
401 $("body").css("cursor", "inherit");
402 }
403 }
404 } else {
405 $("body").css("cursor", "inherit");
406 }
407
408 if (this._dragging && this._hit.handle) {
409 this._drawArrow(this._dragging, this._getRelativePosition(e), this._hit.handle);
410 }
411
412 if (this._nodeClicked) {
413 this._clear();
414 }
415 },
416 _mouseOver: function (e) {
417
418 if (this._dragging) {
419 if ( (e.cyTarget.id() != this._dragging.id()) && e.cyTarget.data().allowConnection || this._hit.handle.allowLoop) {
420 this._hover = e.cyTarget;
421 }
422 } else {
423 this._hover = e.cyTarget;
424 this._showHandles(this._hover);
425 }
426 },
427 _mouseOut: function (e) {
428 if(!this._dragging) {
429 if (this.permanentHandle) {
430 return;
431 }
432
433 this._clear();
434 }
435 this._hover = null;
436 },
437 _removeEdge: function (edge) {
438 edge.off("mousedown");
439 this._cy.remove("#" + edge.id());
440 },
441 _initEdgeEvents: function (edge) {
442 var self = this;
443 edge.on("mousedown", function () {
444 if (self.__lastClick && Date.now() - self.__lastClick < self.DOUBLE_CLICK_INTERVAL) {
445 self._removeEdge(this);
446 }
447 self.__lastClick = Date.now();
448 })
449 },
450 _hitTestHandles: function (e) {
451 var mousePoisition = this._getRelativePosition(e);
452
453 if (this._hover) {
454 var nodeTypeName = this._hover.data().type;
455 if (nodeTypeName) {
456 var handles = this._handles[nodeTypeName] ? this._handles[nodeTypeName] : this._handles["*"];
457
458 for (var i in handles) {
459 var handle = handles[i];
460
461 var position = this._getHandlePosition(handle, this._hover);
462 if (VectorMath.distance(position, mousePoisition) < this.HANDLE_SIZE) {
463 return {
464 handle: handle,
465 position: position
466 };
467 }
468 }
469 }
470 }
471 },
Michael Lando9db40522017-07-22 17:10:02 +0300472 _getHandlePosition: function (handle, target) { //returns the upper left point at which to begin drawing the handle
Michael Landoed64b5e2017-06-09 03:19:04 +0300473 var position = target.renderedPosition();
Michael Lando39a4e0c2017-07-18 20:46:42 +0300474 var width = target.renderedWidth();
475 var height = target.renderedHeight();
Michael Lando9db40522017-07-22 17:10:02 +0300476 var renderedHandleSize = this.HANDLE_SIZE * this._cy.zoom(); //actual number of pixels that handle will use.
Michael Landoed64b5e2017-06-09 03:19:04 +0300477 var xpos = null;
478 var ypos = null;
479
480 switch (handle.positionX) {
481 case "left":
Michael Lando9db40522017-07-22 17:10:02 +0300482 xpos = position.x - width / 2;
Michael Landoed64b5e2017-06-09 03:19:04 +0300483 break;
Michael Lando9db40522017-07-22 17:10:02 +0300484 case "right": //position.x is the exact center of the node. Need to add half the width to get to the right edge. Then, subtract renderedHandleSize to get handle position
485 xpos = position.x + width / 2 - renderedHandleSize;
Michael Landoed64b5e2017-06-09 03:19:04 +0300486 break;
487 case "center":
488 xpos = position.x;
489 break;
490 }
491
492 switch (handle.positionY) {
493 case "top":
Michael Lando9db40522017-07-22 17:10:02 +0300494 ypos = position.y - height / 2;
Michael Landoed64b5e2017-06-09 03:19:04 +0300495 break;
496 case "center":
497 ypos = position.y;
498 break;
499 case "bottom":
Michael Lando9db40522017-07-22 17:10:02 +0300500 ypos = position.y + height / 2;
Michael Landoed64b5e2017-06-09 03:19:04 +0300501 break;
502 }
503
Michael Lando9db40522017-07-22 17:10:02 +0300504 //Determine if handle will be too big and require offset to prevent it from covering too much of the node icon (in which case, move it over by 1/2 the renderedHandleSize, so half the handle overlaps).
505 //Need to use target.width(), which is the size of the node, unrelated to rendered size/zoom
506 var offsetX = (target.width() < 30) ? renderedHandleSize / 2 : 0;
507 var offsetY = (target.height() < 30) ? renderedHandleSize /2 : 0;
508 return {x: xpos + offsetX, y: ypos - offsetY};
Michael Landoed64b5e2017-06-09 03:19:04 +0300509 },
510 _getEdgeCSSByHandle: function (handle) {
511 var color = handle.lineColor ? handle.lineColor : handle.color;
512 return {
513 "line-color": color,
514 "target-arrow-color": color,
515 "line-style": handle.lineStyle? handle.lineStyle: 'solid',
516 "width": handle.width? handle.width : 3
517 };
518 },
519 _getHandleByType: function (type) {
520 for (var i in this._handles) {
521 var byNodeType = this._handles[i];
522 for (var i2 in byNodeType) {
523 var handle = byNodeType[i2];
524 if (handle.type == type) {
525 return handle;
526 }
527 }
528 }
529 },
530 _getRelativePosition: function (e) {
531 var containerPosition = this._$container.offset();
532 return {
533 x: e.pageX - containerPosition.left,
534 y: e.pageY - containerPosition.top
535 }
536 },
537 _checkSingleEdge: function (handle, node) {
538
539 if (handle.noMultigraph) {
540 var edges = this._cy.edges("[source='" + this._hover.id() + "'][target='" + node.id() + "'],[source='" + node.id() + "'][target='" + this._hover.id() + "']");
541
542 for (var i = 0; i < edges.length; i++) {
543 return edges[i];
544 }
545 } else {
546
547 if (handle.single == false) {
548 return;
549 }
550 var edges = this._cy.edges("[source='" + node.id() + "']");
551
552 for (var i = 0; i < edges.length; i++) {
553 if (edges[i].data()["type"] == handle.type) {
554 return edges[i];
555 }
556 }
557 }
558 },
559 _redraw: function () {
560 this._clear();
561 if (this._hover) {
562 this._showHandles(this._hover);
563 }
564 }
565 });
566
567})(this);
568