2 function MGCanvas(canvasDOMObj){
5 this.initGraphicContext(canvasDOMObj);
6 this.tickPerSecond = 30;
8 this.tickTimer = window.setInterval(function(){ that.tick(); }, 1000 / this.tickPerSecond);
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;
20 window.addEventListener('keydown', function(event){
21 switch(event.keyCode){
23 that.moveViewRelative(-10, 0);
26 that.moveViewRelative(10, 0);
29 that.moveViewRelative(0, -10);
32 that.moveViewRelative(0, 10);
37 this.isMouseDown = false;
38 this.mouseDownPosition = new Point2D(0, 0);
39 this.lastMousePosition = new Point2D(0, 0);
40 this.canvas.onmousemove = function (e){
46 that.lastMousePosition = that.getMousePositionOnElement(e);
49 this.canvas.onmousedown = function (e){
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;
65 this.canvas.onmouseup = function (e){
66 that.isMouseDown = false;
69 MGCanvas.prototype = {
70 setGraph: function(gArray){
71 //gArray = [[Node1, Node2, Node3, ...], [[Node1, Node3], [Node3, Node2], ...]]
74 var n = function(identifier){ return that.nodeList.isIncluded(identifier, function(a, b){ return (a.identifier == b); }); };
76 for(var i = 0, iLen = p.length; i < iLen; i++){
77 this.nodeList.push(new MGNode(this, p[i]));
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])));
85 setSourceMemoryDB: function(mdb){
87 this.srcMemoryDB = mdb;
88 mdb.callback_updatedNode = function(t){
90 n = that.nodeList.isIncluded(t.nodeid, function(a, b){ return a.nodeid == b; });
93 n = new MGNode(that, t.identifier);
95 that.nodeList.push(n);
98 n.identifier = t.identifier;
102 bringToCenter: function(){
103 // 重心を求めて、それを表示オフセットに設定する
104 var g = new Point2D(0, 0);
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;
114 this.positionOffset.x = -g.x;
115 this.positionOffset.y = -g.y;
117 this.isEnabledAutomaticTracking = true;
120 this.context.scale(2, 2);
121 this.currentScale *= 2;
124 this.context.scale(0.5, 0.5);
125 this.currentScale *= 0.5;
127 moveViewRelative: function(x, y){
128 this.positionOffset.x += -x;
129 this.positionOffset.y += -y;
131 bringInScreen: function(){
133 var g = new Point2D(0, 0);
134 var f = new Point2D(0, 0);
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;
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){
150 this.positionOffset.x += -g.x;
151 this.positionOffset.y += -g.y;
163 this.srcMemoryDBSyncCount++;
168 if(this.srcMemoryDB && this.srcMemoryDBSyncCount > this.srcMemoryDBSyncPerTick){
169 this.srcMemoryDB.syncDB();
170 this.srcMemoryDBSyncCount = 0;
176 if(this.isEnabledAutomaticTracking && (this.tickCount % 30 == 0)){
177 this.bringInScreen();
182 // View moving with mouse
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
197 for(var i = 0, iLen = p.length; i < iLen; i++){
198 nTemp = this.getVectorLength(p[i].vector);
205 n.ignoreEdgeRepulsion = 10;
213 for(var i = 0, iLen = p.length; i < iLen; i++){
214 this.nodeList[i].tick();
217 for(var i = 0, iLen = p.length; i < iLen; i++){
218 this.edgeList[i].tick();
225 dr = this.displayRect;
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);
231 this.context.translate(this.positionOffset.x, this.positionOffset.y);
234 for(var i = 0, iLen = p.length; i < iLen; i++){
235 this.nodeList[i].draw();
239 for(var i = 0, iLen = p.length; i < iLen; i++){
240 this.edgeList[i].draw();
243 this.context.translate(-this.positionOffset.x, -this.positionOffset.y);
246 getMousePositionOnElement: function(e){
247 // http://tmlife.net/programming/javascript/javascript-mouse-pos.html
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;
255 fillRect: function(x, y, w, h){
256 var d = this.drawCoordinatesInInteger;
257 this.context.fillRect(d(x), d(y), d(w), d(h));
259 strokeRect: function(x, y, w, h){
260 var d = this.drawCoordinatesInInteger;
261 this.context.strokeRect(d(x), d(y), d(w), d(h));
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);
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();
274 this.context.stroke();
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();
284 drawText: function(text, x, y){
286 //前景をstrokeStyleで塗りつぶした文字列を描画する
289 var textsize = this.context.measureText(text);
290 this.context.fillRect(x, y, textsize.width, 20);
292 this.context.fillStyle = this.context.strokeStyle;
293 //fillText引数の座標は文字列の左下!
294 this.context.fillText(text, x, y + 20 - 1);
295 this.context.restore();
297 getVectorLengthP: function(p, q){
298 return this.getVectorLength(this.getVectorP(p, q));
300 getVectorLength: function(a){
301 return Math.sqrt(a.x * a.x + a.y * a.y);
303 getVectorP: function(p, q){
304 return new Point2D(q.x - p.x, q.y - p.y);
306 getUnitVectorP: function(p, q){
307 var e = this.getVectorP(p, q);
308 return this.getUnitVector(e);
310 getUnitVector: function(a){
311 var l = Math.sqrt(a.x * a.x + a.y * a.y);
316 getNormalUnitVectorSideOfP: function(a, b, p){
317 //直線ab上にない点pが存在する側へ向かう単位法線ベクトルを返す。
318 return this.getUnitVector(this.getNormalVectorSideOfP(a, b, p));
320 getNormalVectorSideOfP: function(a, b, p){
321 //直線ab上にない点pが存在する側へ向かう法線ベクトルを返す。
322 //pがab上にある場合は零ベクトルとなる。
323 var n = this.getVectorP(a, b);
329 i = this.getInnerVector2D(n, this.getVectorP(a, p));
331 //この法線ベクトルとapの向きが逆なので反転する。
340 getExteriorVector2D: function(a, b){
341 return a.x * b.y - a.y * b.x;
343 getInnerVector2D: function(a, b){
344 return a.x * b.x + a.y * b.y;
346 getDistanceDotAndLineP: function(p, a, b){
347 // http://www.sousakuba.com/Programming/gs_dot_line_distance.html
354 ab = this.getVectorP(a, b);
355 ap = this.getVectorP(a, p);
357 s = Math.abs(this.getExteriorVector2D(ab, ap));
358 l = this.getVectorLengthP(a, b);
361 s = this.getInnerVector2D(ap, ab);
363 //線分の範囲外なので端点aからの距離に変換
366 d = Math.sqrt(d * d + l * l);
367 } else if(s > l * l){
370 d = Math.sqrt(d * d + l * l);
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;
386 this.positionOffset = new Point2D(0, 0);
388 convertPointToGraphLayerFromCanvasLayerP: function(pCanvas){
389 var p = new Point2D(pCanvas.x, pCanvas.y);
391 p.x -= this.canvas.width / 2;
392 p.y -= this.canvas.height / 2;
394 p.x /= this.currentScale;
395 p.y /= this.currentScale;
398 p.x -= this.positionOffset.x;
399 p.y -= this.positionOffset.y;
403 getNodeAtPointP: function(p){
404 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
406 var nl = this.nodeList;
407 for(var i = 0, iLen = nl.length; i < iLen; i++){
408 if(r.isIncludePointP(nl[i].position)){
414 getEdgeAtPointP: function(p){
415 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
417 var el = this.edgeList;
418 for(var i = 0, iLen = el.length; i < iLen; i++){
419 if(r.isIncludePointP(el[i].centerPoint)){
425 selectNode: function(node){
426 if(this.selectedNode){
427 this.selectedNode.isSelected = false;
430 node.isSelected = true;
432 this.selectedNode = node;
434 selectEdge: function(edge){
435 if(this.selectedEdge){
436 this.selectedEdge.isSelected = false;
439 edge.isSelected = true;
441 this.selectedEdge = edge;
443 setIdentifierForSelectedNode: function(str){
444 if(this.selectedNode){
445 if(this.srcMemoryDB){
446 this.srcMemoryDB.updateNode(str, this.selectedNode.typeid, this.selectedNode.nodeid);
448 this.selectedNode.identifier = str;
454 function MGNode(env, identifier){
457 this.identifier = identifier;
459 this.nodeid = undefined;
460 this.typeid = undefined;
462 this.position = new Point2D(Math.random() * 32 - 16, Math.random() * 32 - 16);
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;
471 this.strokeStyle = "rgba(0, 0, 0, 1)";
472 this.isSelected = false;
477 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
479 this.env.context.strokeStyle = this.strokeStyle;
481 this.env.drawCircle(this.position.x, this.position.y, this.size);
483 this.env.drawText(this.identifier.toString(), this.position.x + 10, this.position.y + 10);
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;
496 if(!this.ignoreEdgeRepulsion){
498 //距離の近い点同士には斥力が働くとする。
499 p = this.env.nodeList;
500 for(var i = 0, iLen = p.length; i < iLen; i++){
501 var q = this.env.nodeList[i];
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;
515 //自分を端点に含まないエッジとの距離が近い場合、そのエッジから遠ざかろうとする。
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){
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;
540 this.ignoreEdgeRepulsion--;
546 function MGEdge(env, identifier, node0, node1){
548 this.identifier = identifier;
551 this.freeLength = 250;
553 this.strokeStyle = "rgba(0, 0, 0, 0.5)";
554 this.isSelected = false;
556 this.centerPoint = new Point2D(0, 0);
561 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
563 this.env.context.strokeStyle = this.strokeStyle;
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);
569 this.env.drawText(this.identifier.toString(), this.centerPoint.x, this.centerPoint.y);
574 var l = this.env.getVectorLengthP(this.node0.position, this.node1.position);
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;
585 this.centerPoint = new Point2D((this.node0.position.x + this.node1.position.x) / 2, (this.node0.position.y + this.node1.position.y) / 2);
587 drawCurvedLineP: function(p, q){
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();
600 function Point2D(x, y){
604 Point2D.prototype = {
608 function Rectangle(x, y, width, height){
609 this.origin = new Point2D(x,y);
610 this.size = new Point2D(width,height);
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);