OSDN Git Service

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