OSDN Git Service

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