OSDN Git Service

28fac9626837237c6a8bf31f9561016998241931
[chnosproject/AI004.git] / mgcanvas / mgcanvas.js
1
2 function MGCanvas(canvasDOMObj){
3         var that = this;
4
5         this.initGraphicContext(canvasDOMObj);
6         this.tickPerSecond = 30;
7         this.tickCount = 0;
8         this.tickTimer = window.setInterval(function(){ that.tick(); }, 1000 / this.tickPerSecond);
9         this.isPaused = false;
10         this.isEnabledAutomaticTracking = true;
11         this.nodeList = new Array();
12         this.edgeList = new Array();
13         this.selectedNode = null;
14         this.selectedEdge = null;
15         
16         var that = this;
17         window.addEventListener('keydown', function(event){
18                 switch(event.keyCode){
19                         case 37:        //左カーソル
20                                 that.moveViewRelative(-10, 0);
21                                 break;
22                         case 39:        //右カーソル
23                                 that.moveViewRelative(10, 0);
24                                 break;
25                         case 38:        //上カーソル
26                                 that.moveViewRelative(0, -10);
27                                 break;
28                         case 40:        //下カーソル
29                                 that.moveViewRelative(0, 10);
30                                 break;
31                 }
32         }, true);
33         
34         this.isMouseDown = false;
35         this.mouseDownPosition = new Point2D(0, 0);
36         this.lastMousePosition = new Point2D(0, 0);
37         this.canvas.onmousemove = function (e){
38                 if(that.isMouseDown){
39                         if(!e){
40                                 //for IE
41                                 e = window.event;
42                         }
43                         that.lastMousePosition = that.getMousePositionOnElement(e);
44                 }
45         };
46         this.canvas.onmousedown = function (e){
47                 if(!e){
48                         //for IE
49                         e = window.event;
50                 }
51                 that.lastMousePosition = that.getMousePositionOnElement(e);
52                 that.mouseDownPosition = that.lastMousePosition;
53                 var p = that.convertPointToGraphLayerFromCanvasLayerP(that.lastMousePosition);
54                 //console.log(p.x + "," + p.y);
55                 var node = that.getNodeAtPointP(p);
56                 that.selectNode(node);
57                 var edge = that.getEdgeAtPointP(p);
58                 that.selectEdge(edge);
59                 that.isMouseDown = true;
60                 that.isEnabledAutomaticTracking = false;
61         };
62         this.canvas.onmouseup = function (e){
63                 that.isMouseDown = false;
64         };
65 }
66 MGCanvas.prototype = {
67         setGraph: function(gArray){
68                 //gArray = [[Node1, Node2, Node3, ...], [[Node1, Node3], [Node3, Node2], ...]]
69                 var that = this;
70                 var p = gArray[0];
71                 var n = function(identifier){ return that.nodeList.isIncluded(identifier, function(a, b){ return (a.identifier == b); }); };
72                 
73                 for(var i = 0, iLen = p.length; i < iLen; i++){
74                         this.nodeList.push(new MGNode(this, p[i]));
75                 }
76                 
77                 p = gArray[1];
78                 for(var i = 0, iLen = p.length; i < iLen; i++){
79                         this.edgeList.push(new MGEdge(this, p[i][2], n(p[i][0]), n(p[i][1])));
80                 }
81         },
82         loadGraphFromMemoryDB: function(mdb){
83                 var a, l, t, u, m, e, f;
84                 a = mdb.nodeList;
85                 l = a.length;
86                 //m = 12 * l;
87                 t = new MGNode(this, "Node");
88                 this.nodeList.push(t);
89                 f = function(){};
90                 for(var i = 0; i < l; i++){
91                         u = new MGNode(this, a[i].identifier);
92                         this.nodeList.push(u);
93                         //e = new MGEdge(this, null, t, u);
94                         //this.edgeList.push(e);
95                         //t = u;
96                         //e.freeLength = m;
97                         //e.draw = f;
98                 }
99         },
100         bringToCenter: function(){
101                 // 重心を求めて、それを表示オフセットに設定する
102                 var g = new Point2D(0, 0);
103                 var p;
104                 p = this.nodeList;
105                 for(var i = 0, iLen = p.length; i < iLen; i++){
106                         g.x += p[i].position.x;
107                         g.y += p[i].position.y;
108                 }
109                 g.x /= p.length;
110                 g.y /= p.length;
111                 
112                 this.positionOffset.x = -g.x;
113                 this.positionOffset.y = -g.y;
114                 
115                 this.isEnabledAutomaticTracking = true;
116         },
117         zoomIn: function(){
118                 this.context.scale(2, 2);
119                 this.currentScale *= 2;
120         },
121         zoomOut: function(){
122                 this.context.scale(0.5, 0.5);
123                 this.currentScale *= 0.5;
124         },
125         moveViewRelative: function(x, y){
126                 this.positionOffset.x += -x;
127                 this.positionOffset.y += -y;
128         },
129         bringInScreen: function(){
130                 //大きく外れていないときには動かさない
131                 var g = new Point2D(0, 0);
132                 var f = new Point2D(0, 0);
133                 var p;
134                 p = this.nodeList;
135                 for(var i = 0, iLen = p.length; i < iLen; i++){
136                         g.x += p[i].position.x;
137                         g.y += p[i].position.y;
138                 }
139                 g.x /= p.length;
140                 g.y /= p.length;
141                 g.x += this.positionOffset.x;
142                 g.y += this.positionOffset.y;
143                 if(     g.x < this.displayRect.origin.x / 2 || 
144                         g.x > -this.displayRect.origin.x / 2 || 
145                         g.y < this.displayRect.origin.y / 2 || 
146                         g.y > -this.displayRect.origin.x / 2){
147                         
148                         this.positionOffset.x += -g.x;
149                         this.positionOffset.y += -g.y;
150                 }
151         },
152         tick: function(){
153                 var p;
154                 var t;
155                 var dr;
156                 var n = null;
157                 var nMax = 0;
158                 var nTemp;
159                 
160                 this.tickCount++;
161                 
162                 //
163                 // AutomaticTracking
164                 //
165                 
166                 if(this.isEnabledAutomaticTracking && (this.tickCount % 30 == 0)){
167                         this.bringInScreen();
168                 }
169
170                 
171                 //
172                 // View moving with mouse
173                 //
174                 if(this.isMouseDown){
175                         this.moveViewRelative(
176                                 (this.mouseDownPosition.x - this.lastMousePosition.x) * 4 / this.tickPerSecond,
177                                 (this.mouseDownPosition.y - this.lastMousePosition.y) * 4 / this.tickPerSecond
178                         );
179                 }
180                 
181                 if(!this.isPaused){
182                         //
183                         // Check
184                         //
185                         /*
186                         p = this.nodeList;
187                         for(var i = 0, iLen = p.length; i < iLen; i++){
188                                 nTemp = this.getVectorLength(p[i].vector);
189                                 if(nMax < nTemp){
190                                         n = p[i];
191                                         nMax = nTemp;
192                                 }
193                         }
194                         if(n){
195                                 n.ignoreEdgeRepulsion = 10;
196                         }
197                         */
198                         
199                         //
200                         // Move
201                         //
202                         p = this.nodeList;
203                         for(var i = 0, iLen = p.length; i < iLen; i++){
204                                 this.nodeList[i].tick();
205                         }
206                         p = this.edgeList;
207                         for(var i = 0, iLen = p.length; i < iLen; i++){
208                                 this.edgeList[i].tick();
209                         }
210                 }
211                 
212                 //
213                 // Refresh
214                 //
215                 dr = this.displayRect;
216                 
217                 this.context.scale(1 / this.currentScale, 1 / this.currentScale);
218                 this.context.clearRect(dr.origin.x, dr.origin.y, dr.size.x, dr.size.y);
219                 this.context.scale(this.currentScale, this.currentScale);
220                 
221                 this.context.translate(this.positionOffset.x, this.positionOffset.y);
222                 
223                 p = this.nodeList;
224                 for(var i = 0, iLen = p.length; i < iLen; i++){
225                         this.nodeList[i].draw();
226                 }
227                 
228                 p = this.edgeList;
229                 for(var i = 0, iLen = p.length; i < iLen; i++){
230                         this.edgeList[i].draw();
231                 }
232                 
233                 this.context.translate(-this.positionOffset.x, -this.positionOffset.y);
234                 
235         },
236         getMousePositionOnElement: function(e){
237                 // http://tmlife.net/programming/javascript/javascript-mouse-pos.html
238                 // retv:
239                 var retv = new Object();
240                 var rect = e.target.getBoundingClientRect();
241                 retv.x = e.clientX - rect.left;
242                 retv.y = e.clientY - rect.top;
243                 return retv;
244         },
245         fillRect: function(x, y, w, h){
246                 var d = this.drawCoordinatesInInteger;
247                 this.context.fillRect(d(x), d(y), d(w), d(h));
248         },
249         strokeRect: function(x, y, w, h){
250                 var d = this.drawCoordinatesInInteger;
251                 this.context.strokeRect(d(x), d(y), d(w), d(h));
252         },
253         drawCoordinatesInInteger: function(coordinateElement){
254                 //from http://www.html5rocks.com/ja/tutorials/canvas/performance/
255                 // With a bitwise or.
256                 return ((0.5 + coordinateElement) | 0);
257         },
258         drawCircle: function(x, y, r){
259                 var d = this.drawCoordinatesInInteger;
260                 this.context.beginPath();
261                 this.context.arc(d(x), d(y), d(r), 0, 2 * Math.PI);
262                 this.context.closePath();
263                 this.context.fill();
264                 this.context.stroke();
265         },
266         drawLineP: function(p, q){
267                 var d = this.drawCoordinatesInInteger;
268                 this.context.beginPath();
269                 this.context.moveTo(d(p.x), d(p.y));
270                 this.context.lineTo(d(q.x), d(q.y));
271                 this.context.closePath();
272                 this.context.stroke();
273         },
274         drawText: function(text, x, y){
275                 //背景をfillStyle
276                 //前景をstrokeStyleで塗りつぶした文字列を描画する
277                 //塗りつぶし高さは20px固定
278                 //座標は文字列の左上の座標となる
279                 var textsize = this.context.measureText(text);
280                 this.context.fillRect(x, y, textsize.width, 20);
281                 this.context.save();
282                 this.context.fillStyle = this.context.strokeStyle;
283                 //fillText引数の座標は文字列の左下!
284                 this.context.fillText(text, x, y + 20 - 1);
285                 this.context.restore();
286         },
287         getVectorLengthP: function(p, q){
288                 return this.getVectorLength(this.getVectorP(p, q));
289         },
290         getVectorLength: function(a){
291                 return Math.sqrt(a.x * a.x + a.y * a.y);
292         },
293         getVectorP: function(p, q){
294                 return new Point2D(q.x - p.x, q.y - p.y);
295         },
296         getUnitVectorP: function(p, q){
297                 var e = this.getVectorP(p, q);
298                 return this.getUnitVector(e);
299         },
300         getUnitVector: function(a){
301                 var l = Math.sqrt(a.x * a.x + a.y * a.y);
302                 a.x /= l;
303                 a.y /= l;
304                 return a;
305         },
306         getNormalUnitVectorSideOfP: function(a, b, p){
307                 //直線ab上にない点pが存在する側へ向かう単位法線ベクトルを返す。
308                 return this.getUnitVector(this.getNormalVectorSideOfP(a, b, p));
309         },
310         getNormalVectorSideOfP: function(a, b, p){
311                 //直線ab上にない点pが存在する側へ向かう法線ベクトルを返す。
312                 //pがab上にある場合は零ベクトルとなる。
313                 var n = this.getVectorP(a, b);
314                 var t = n.x;
315                 var i;
316                 n.x = -n.y;
317                 n.y = t;
318                 
319                 i = this.getInnerVector2D(n, this.getVectorP(a, p));
320                 if(i < 0){
321                         //この法線ベクトルとapの向きが逆なので反転する。
322                         n.x = -n.x;
323                         n.y = -n.y;
324                 } else if(i == 0){
325                         n.x = 0;
326                         n.y = 0;
327                 }
328                 return n;
329         },
330         getExteriorVector2D: function(a, b){
331                 return a.x * b.y - a.y * b.x;
332         },
333         getInnerVector2D: function(a, b){
334                 return a.x * b.x + a.y * b.y;
335         },
336         getDistanceDotAndLineP: function(p, a, b){
337                 // http://www.sousakuba.com/Programming/gs_dot_line_distance.html
338                 var ab;
339                 var ap;
340                 var s;
341                 var l;
342                 var d;
343                 
344                 ab = this.getVectorP(a, b);
345                 ap = this.getVectorP(a, p);
346                 
347                 s = Math.abs(this.getExteriorVector2D(ab, ap));
348                 l = this.getVectorLengthP(a, b);
349                 d = (s / l);
350                 
351                 s = this.getInnerVector2D(ap, ab);
352                 if(s < 0){
353                         //線分の範囲外なので端点aからの距離に変換
354                         //端点から垂線の足までの距離
355                         l = - (s / l);
356                         d = Math.sqrt(d * d + l * l);
357                 } else if(s > l * l){
358                         //同様に端点bからの距離に変換
359                         l = s / l;
360                         d = Math.sqrt(d * d + l * l);
361                 }
362                 return d;
363         },
364         initGraphicContext: function(newCanvas){
365                 this.canvas = newCanvas;
366                 this.context = this.canvas.getContext('2d');
367                 this.context.fillStyle = "rgba(255,255,255,0.5)";
368                 this.context.strokeStyle = "rgba(0, 0, 0, 1)";
369                 this.context.font = "normal 20px sans-serif";
370                 var w = this.canvas.width / 2;
371                 var h = this.canvas.height / 2;
372                 this.context.translate(w, h);
373                 this.displayRect = new Rectangle(-w, -h, this.canvas.width, this.canvas.height);
374                 this.currentScale = 1;
375                 this.zoomOut();
376                 this.positionOffset = new Point2D(0, 0);
377         },
378         convertPointToGraphLayerFromCanvasLayerP: function(pCanvas){
379                 var p = new Point2D(pCanvas.x, pCanvas.y);
380                 // Canvasの中心が原点
381                 p.x -= this.canvas.width / 2;
382                 p.y -= this.canvas.height / 2;
383                 // スケール変換
384                 p.x /= this.currentScale;
385                 p.y /= this.currentScale;
386                 
387                 // オフセット平行移動
388                 p.x -= this.positionOffset.x;
389                 p.y -= this.positionOffset.y;
390                 
391                 return p;
392         },
393         getNodeAtPointP: function(p){
394                 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
395                 
396                 var nl = this.nodeList;
397                 for(var i = 0, iLen = nl.length; i < iLen; i++){
398                         if(r.isIncludePointP(nl[i].position)){
399                                 return nl[i];
400                         }
401                 }
402                 return null;
403         },
404         getEdgeAtPointP: function(p){
405                 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
406                 
407                 var el = this.edgeList;
408                 for(var i = 0, iLen = el.length; i < iLen; i++){
409                         if(r.isIncludePointP(el[i].centerPoint)){
410                                 return el[i];
411                         }
412                 }
413                 return null;
414         },
415         selectNode: function(node){
416                 if(this.selectedNode){
417                         this.selectedNode.isSelected = false;
418                 }
419                 if(node){
420                         node.isSelected = true;
421                 }
422                 this.selectedNode = node;
423         },
424         selectEdge: function(edge){
425                 if(this.selectedEdge){
426                         this.selectedEdge.isSelected = false;
427                 }
428                 if(edge){
429                         edge.isSelected = true;
430                 }
431                 this.selectedEdge = edge;
432         },
433         setIdentifierForSelectedNode: function(str){
434                 if(this.selectedNode){
435                         this.selectedNode.identifier = str;
436                 }
437         },
438 }
439
440 function MGNode(env, identifier){
441         this.env = env;
442         this.identifier = identifier;
443         this.position = new Point2D(Math.random() * 32 - 16, Math.random() * 32 - 16);
444         this.size = 10;
445         //ランダムな初期ベクトルをもつ。
446         this.vector = new Point2D(Math.random() * 2 - 1, Math.random() * 2 - 1);
447         this.friction = 50 / 100;
448         this.repulsionLengthNode = 90;
449         this.repulsionLengthEdge = 90;
450         this.ignoreEdgeRepulsion = 0;
451         //
452         this.strokeStyle = "rgba(0, 0, 0, 1)";
453         this.isSelected = false;
454 }
455 MGNode.prototype = {
456         draw: function(){
457                 if(this.isSelected){
458                         this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
459                 } else{
460                         this.env.context.strokeStyle = this.strokeStyle;
461                 }
462                 this.env.drawCircle(this.position.x, this.position.y, this.size);
463                 if(this.identifier){
464                         this.env.drawText(this.identifier.toString(), this.position.x + 10, this.position.y + 10);
465                 }
466         },
467         tick: function(){
468                 var e;
469                 var p;
470                 var l;
471                 var q;
472                 this.position.x += this.vector.x;
473                 this.position.y += this.vector.y;
474                 this.vector.x *= this.friction;
475                 this.vector.y *= this.friction;
476                 
477                 if(!this.ignoreEdgeRepulsion){
478                         //node
479                         //距離の近い点同士には斥力が働くとする。
480                         p = this.env.nodeList;
481                         for(var i = 0, iLen = p.length; i < iLen; i++){
482                                 var q = this.env.nodeList[i];
483                                 if(q != this){
484                                         l = this.env.getVectorLengthP(this.position, q.position);
485                                         if(l < this.repulsionLengthNode && l != 0){
486                                                 e = this.env.getUnitVectorP(q.position, this.position);
487                                                 e.x *= this.repulsionLengthNode / l;
488                                                 e.y *= this.repulsionLengthNode / l;
489                                                 this.vector.x += e.x;
490                                                 this.vector.y += e.y;
491                                         }
492                                 }
493                         }
494                         
495                         //edge
496                         //自分を端点に含まないエッジとの距離が近い場合、そのエッジから遠ざかろうとする。
497                         /*
498                         p = this.env.edgeList;
499                         for(var i = 0, iLen = p.length; i < iLen; i++){
500                                 var q = this.env.edgeList[i];
501                                 if(q.node0 != this && q.node1 != this){
502                                         l = this.env.getDistanceDotAndLineP(this.position, q.node0.position, q.node1.position);
503                                         if(l < this.repulsionLengthEdge && l != 0){
504                                                 if(l < 1){
505                                                         l = 1;
506                                                 }
507                                                 e = this.env.getNormalUnitVectorSideOfP(q.node0.position, q.node1.position, this.position);
508                                                 e.x *= this.repulsionLengthEdge / l;
509                                                 e.y *= this.repulsionLengthEdge / l;
510                                                 this.vector.x += e.x;
511                                                 this.vector.y += e.y;
512                                                 q.node0.vector.x -= e.x / 2;
513                                                 q.node0.vector.y -= e.y / 2;
514                                                 q.node1.vector.x -= e.x / 2;
515                                                 q.node1.vector.y -= e.y / 2;
516                                         }
517                                 }
518                         }
519                         */
520                 } else{
521                         this.ignoreEdgeRepulsion--;
522                 }
523                 
524         },
525 }
526
527 function MGEdge(env, identifier, node0, node1){
528         this.env = env;
529         this.identifier = identifier;
530         this.node0 = node0;
531         this.node1 = node1;
532         this.freeLength = 250;
533         //
534         this.strokeStyle = "rgba(0, 0, 0, 0.5)";
535         this.isSelected = false;
536         //
537         this.centerPoint = new Point2D(0, 0);
538 }
539 MGEdge.prototype = {
540         draw: function(){
541                 if(this.isSelected){
542                         this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
543                 } else{
544                         this.env.context.strokeStyle = this.strokeStyle;
545                 }
546                 if(this.node0 && this.node1){
547                         this.drawCurvedLineP(this.node0.position, this.node1.position);
548                         this.env.strokeRect(this.centerPoint.x - 8, this.centerPoint.y - 8, 16, 16);
549                         if(this.identifier){
550                                 this.env.drawText(this.identifier.toString(), this.centerPoint.x, this.centerPoint.y);
551                         }
552                 }
553         },
554         tick: function(){
555                 var l = this.env.getVectorLengthP(this.node0.position, this.node1.position);
556                 var e;
557                 if(l > this.freeLength){
558                         e = this.env.getUnitVectorP(this.node0.position, this.node1.position);
559                         e.x *= l / this.freeLength;
560                         e.y *= l / this.freeLength;
561                         this.node0.vector.x += e.x;
562                         this.node0.vector.y += e.y;
563                         this.node1.vector.x -= e.x;
564                         this.node1.vector.y -= e.y;
565                 }
566                 this.centerPoint = new Point2D((this.node0.position.x + this.node1.position.x) / 2, (this.node0.position.y + this.node1.position.y) / 2);
567         },
568         drawCurvedLineP: function(p, q){
569                 var that = this;
570                 var d = function(x){ return that.env.drawCoordinatesInInteger(x); };
571                 var v = this.env.getUnitVectorP(p, q);
572                 var w = new Point2D(-(v.y * 50), v.x * 50);
573                 this.env.context.beginPath();
574                 this.env.context.moveTo(d(p.x), d(p.y));
575                 this.env.context.bezierCurveTo(d(this.centerPoint.x + w.x), d(this.centerPoint.y + w.y), d(this.centerPoint.x - w.x), d(this.centerPoint.y - w.y), d(q.x), d(q.y));
576                 //this.env.context.closePath();
577                 this.env.context.stroke();
578         },
579 }
580
581 function Point2D(x, y){
582         this.x = x;
583         this.y = y;
584 }
585 Point2D.prototype = {
586         
587 }
588
589 function Rectangle(x, y, width, height){
590         this.origin = new Point2D(x,y);
591         this.size = new Point2D(width,height);
592 }
593 Rectangle.prototype = {
594         isIncludePointP: function(p){
595                 return  (this.origin.x <= p.x) && (p.x <= this.origin.x + this.size.x) &&
596                                 (this.origin.y <= p.y) && (p.y <= this.origin.y + this.size.y);
597         },
598 }
599