OSDN Git Service

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