2 function MGCanvas(canvasDOMObj){
5 this.initGraphicContext(canvasDOMObj);
7 this.tickPerSecond = 30;
9 this.tickTimer = window.setInterval(function(){ that.tick(); }, 1000 / this.tickPerSecond);
10 this.isPaused = false;
11 this.isEnabledAutomaticTracking = true;
13 this.nodeList = new Array();
14 this.edgeList = new Array();
16 this.selectedNode = null;
17 this.callback_selectedNodeChanged = null; //function(newNodeInstance){};
19 this.selectedNodeDestination = null;
20 this.callback_selectedNodeDestinationChanged = null; //function(newNodeInstance){};
22 this.selectedEdge = null;
23 this.callback_selectedEdgeChanged = null; //function(newEdgeInstance){};
25 this.srcMemoryDB = null;
26 this.srcMemoryDBSyncPerTick = 60;
27 this.srcMemoryDBSyncCount = this.srcMemoryDBSyncPerTick;
29 window.addEventListener('keydown', function(event){
30 switch(event.keyCode){
32 that.moveViewRelative(-10, 0);
35 that.moveViewRelative(10, 0);
38 that.moveViewRelative(0, -10);
41 that.moveViewRelative(0, 10);
46 this.isMouseDown = false;
47 this.mouseDownPosition = new Point2D(0, 0);
48 this.lastMousePosition = new Point2D(0, 0);
49 this.canvas.onmousemove = function (e){
55 that.lastMousePosition = that.getMousePositionOnElement(e);
58 this.canvas.onmousedown = function (e){
63 that.lastMousePosition = that.getMousePositionOnElement(e);
64 that.mouseDownPosition = that.lastMousePosition;
65 var p = that.convertPointToGraphLayerFromCanvasLayerP(that.lastMousePosition);
66 //console.log(p.x + "," + p.y);
67 var node = that.getNodeAtPointP(p);
68 that.selectNode(node, e.shiftKey);
69 var edge = that.getEdgeAtPointP(p);
70 that.selectEdge(edge);
71 that.isMouseDown = true;
72 that.isEnabledAutomaticTracking = false;
74 this.canvas.onmouseup = function (e){
75 that.isMouseDown = false;
78 MGCanvas.prototype = {
79 setGraph: function(gArray){
80 //gArray = [[Node1, Node2, Node3, ...], [[Node1, Node3], [Node3, Node2], ...]]
83 var n = function(identifier){ return that.nodeList.isIncluded(identifier, function(a, b){ return (a.identifier == b); }); };
85 for(var i = 0, iLen = p.length; i < iLen; i++){
86 this.nodeList.push(new MGNode(this, p[i]));
90 for(var i = 0, iLen = p.length; i < iLen; i++){
91 this.edgeList.push(new MGEdge(this, p[i][2], n(p[i][0]), n(p[i][1])));
94 setSourceMemoryDB: function(mdb){
96 this.srcMemoryDB = mdb;
97 mdb.callback_updatedNode = function(t){
99 n = that.nodeList.isIncluded(t.nodeid, function(a, b){ return a.nodeid == b; });
102 n = new MGNode(that, t.identifier);
105 that.nodeList.push(n);
108 n.identifier = t.identifier;
112 mdb.callback_updatedEdge = function(t){
114 var n = function(nid){ return that.nodeList.isIncluded(nid, function(a, b){ return (a.nodeid == b); }); };
115 e = that.edgeList.isIncluded(t.edgeid, function(a, b){ return a.edgeid == b; });
118 e = new MGEdge(that, "", n(t.nodeid0), n(t.nodeid1));
121 that.edgeList.push(e);
125 e.node0 = n(t.nodeid0);
126 e.node1 = n(t.nodeid1);
131 bringToCenter: function(){
132 // 重心を求めて、それを表示オフセットに設定する
133 var g = new Point2D(0, 0);
136 for(var i = 0, iLen = p.length; i < iLen; i++){
137 g.x += p[i].position.x;
138 g.y += p[i].position.y;
143 this.positionOffset.x = -g.x;
144 this.positionOffset.y = -g.y;
146 this.isEnabledAutomaticTracking = true;
149 this.context.scale(2, 2);
150 this.currentScale *= 2;
153 this.context.scale(0.5, 0.5);
154 this.currentScale *= 0.5;
156 moveViewRelative: function(x, y){
157 this.positionOffset.x += -x;
158 this.positionOffset.y += -y;
160 bringInScreen: function(){
162 var g = new Point2D(0, 0);
163 var f = new Point2D(0, 0);
166 for(var i = 0, iLen = p.length; i < iLen; i++){
167 g.x += p[i].position.x;
168 g.y += p[i].position.y;
172 g.x += this.positionOffset.x;
173 g.y += this.positionOffset.y;
174 if( g.x < this.displayRect.origin.x / 2 ||
175 g.x > -this.displayRect.origin.x / 2 ||
176 g.y < this.displayRect.origin.y / 2 ||
177 g.y > -this.displayRect.origin.x / 2){
179 this.positionOffset.x += -g.x;
180 this.positionOffset.y += -g.y;
192 this.srcMemoryDBSyncCount++;
197 if(this.srcMemoryDB && this.srcMemoryDBSyncCount > this.srcMemoryDBSyncPerTick){
198 this.srcMemoryDB.syncDB();
199 this.srcMemoryDBSyncCount = 0;
205 if(this.isEnabledAutomaticTracking && (this.tickCount % 30 == 0)){
206 this.bringInScreen();
211 // View moving with mouse
213 if(this.isMouseDown){
214 this.moveViewRelative(
215 (this.mouseDownPosition.x - this.lastMousePosition.x) * 4 / this.tickPerSecond,
216 (this.mouseDownPosition.y - this.lastMousePosition.y) * 4 / this.tickPerSecond
226 for(var i = 0, iLen = p.length; i < iLen; i++){
227 nTemp = this.getVectorLength(p[i].vector);
234 n.ignoreEdgeRepulsion = 10;
242 for(var i = 0, iLen = p.length; i < iLen; i++){
243 this.nodeList[i].tick();
246 for(var i = 0, iLen = p.length; i < iLen; i++){
247 this.edgeList[i].tick();
254 dr = this.displayRect;
256 this.context.scale(1 / this.currentScale, 1 / this.currentScale);
257 this.context.clearRect(dr.origin.x, dr.origin.y, dr.size.x, dr.size.y);
258 this.context.scale(this.currentScale, this.currentScale);
260 this.context.translate(this.positionOffset.x, this.positionOffset.y);
263 for(var i = 0, iLen = p.length; i < iLen; i++){
264 this.nodeList[i].draw();
268 for(var i = 0, iLen = p.length; i < iLen; i++){
269 this.edgeList[i].draw();
272 this.context.translate(-this.positionOffset.x, -this.positionOffset.y);
275 getMousePositionOnElement: function(e){
276 // http://tmlife.net/programming/javascript/javascript-mouse-pos.html
278 var retv = new Object();
279 var rect = e.target.getBoundingClientRect();
280 retv.x = e.clientX - rect.left;
281 retv.y = e.clientY - rect.top;
284 fillRect: function(x, y, w, h){
285 var d = this.drawCoordinatesInInteger;
286 this.context.fillRect(d(x), d(y), d(w), d(h));
288 strokeRect: function(x, y, w, h){
289 var d = this.drawCoordinatesInInteger;
290 this.context.strokeRect(d(x), d(y), d(w), d(h));
292 drawCoordinatesInInteger: function(coordinateElement){
293 //from http://www.html5rocks.com/ja/tutorials/canvas/performance/
294 // With a bitwise or.
295 return ((0.5 + coordinateElement) | 0);
297 drawCircle: function(x, y, r){
298 var d = this.drawCoordinatesInInteger;
299 this.context.beginPath();
300 this.context.arc(d(x), d(y), d(r), 0, 2 * Math.PI);
301 this.context.closePath();
303 this.context.stroke();
305 drawLineP: function(p, q){
306 var d = this.drawCoordinatesInInteger;
307 this.context.beginPath();
308 this.context.moveTo(d(p.x), d(p.y));
309 this.context.lineTo(d(q.x), d(q.y));
310 this.context.closePath();
311 this.context.stroke();
313 drawText: function(text, x, y){
315 //前景をstrokeStyleで塗りつぶした文字列を描画する
318 var textsize = this.context.measureText(text);
319 this.context.fillRect(x, y, textsize.width, 20);
321 this.context.fillStyle = this.context.strokeStyle;
322 //fillText引数の座標は文字列の左下!
323 this.context.fillText(text, x, y + 20 - 1);
324 this.context.restore();
326 getVectorLengthP: function(p, q){
327 return this.getVectorLength(this.getVectorP(p, q));
329 getVectorLength: function(a){
330 return Math.sqrt(a.x * a.x + a.y * a.y);
332 getVectorP: function(p, q){
333 return new Point2D(q.x - p.x, q.y - p.y);
335 getUnitVectorP: function(p, q){
336 var e = this.getVectorP(p, q);
337 return this.getUnitVector(e);
339 getUnitVector: function(a){
340 var l = Math.sqrt(a.x * a.x + a.y * a.y);
345 getNormalUnitVectorSideOfP: function(a, b, p){
346 //直線ab上にない点pが存在する側へ向かう単位法線ベクトルを返す。
347 return this.getUnitVector(this.getNormalVectorSideOfP(a, b, p));
349 getNormalVectorSideOfP: function(a, b, p){
350 //直線ab上にない点pが存在する側へ向かう法線ベクトルを返す。
351 //pがab上にある場合は零ベクトルとなる。
352 var n = this.getVectorP(a, b);
358 i = this.getInnerVector2D(n, this.getVectorP(a, p));
360 //この法線ベクトルとapの向きが逆なので反転する。
369 getExteriorVector2D: function(a, b){
370 return a.x * b.y - a.y * b.x;
372 getInnerVector2D: function(a, b){
373 return a.x * b.x + a.y * b.y;
375 getDistanceDotAndLineP: function(p, a, b){
376 // http://www.sousakuba.com/Programming/gs_dot_line_distance.html
383 ab = this.getVectorP(a, b);
384 ap = this.getVectorP(a, p);
386 s = Math.abs(this.getExteriorVector2D(ab, ap));
387 l = this.getVectorLengthP(a, b);
390 s = this.getInnerVector2D(ap, ab);
392 //線分の範囲外なので端点aからの距離に変換
395 d = Math.sqrt(d * d + l * l);
396 } else if(s > l * l){
399 d = Math.sqrt(d * d + l * l);
403 initGraphicContext: function(newCanvas){
404 this.canvas = newCanvas;
405 this.context = this.canvas.getContext('2d');
406 this.context.fillStyle = "rgba(255,255,255,0.5)";
407 this.context.strokeStyle = "rgba(0, 0, 0, 1)";
408 this.context.font = "normal 20px sans-serif";
409 var w = this.canvas.width / 2;
410 var h = this.canvas.height / 2;
411 this.context.translate(w, h);
412 this.displayRect = new Rectangle(-w, -h, this.canvas.width, this.canvas.height);
413 this.currentScale = 1;
415 this.positionOffset = new Point2D(0, 0);
417 convertPointToGraphLayerFromCanvasLayerP: function(pCanvas){
418 var p = new Point2D(pCanvas.x, pCanvas.y);
420 p.x -= this.canvas.width / 2;
421 p.y -= this.canvas.height / 2;
423 p.x /= this.currentScale;
424 p.y /= this.currentScale;
427 p.x -= this.positionOffset.x;
428 p.y -= this.positionOffset.y;
432 getNodeAtPointP: function(p){
433 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
435 var nl = this.nodeList;
436 for(var i = 0, iLen = nl.length; i < iLen; i++){
437 if(r.isIncludePointP(nl[i].position)){
443 getEdgeAtPointP: function(p){
444 var r = new Rectangle(p.x - 10, p.y - 10, 20, 20);
446 var el = this.edgeList;
447 for(var i = 0, iLen = el.length; i < iLen; i++){
448 if(r.isIncludePointP(el[i].centerPoint)){
454 selectNode: function(node, destNode){
457 if(this.selectedNode == node){
460 if(this.selectedNode){
461 this.selectedNode.isSelected = false;
464 node.isSelected = true;
466 this.selectedNode = node;
467 if(this.callback_selectedNodeChanged){
468 this.callback_selectedNodeChanged(this.selectedNode);
471 // 接続先のノード選択(Shiftキーを押しながら)
472 if(this.selectedNodeDestination == node){
475 if(this.selectedNodeDestination){
476 this.selectedNodeDestination.isSelected = false;
479 node.isSelected = true;
481 this.selectedNodeDestination = node;
482 if(this.callback_selectedNodeDestinationChanged){
483 this.callback_selectedNodeDestinationChanged(this.selectedNodeDestination);
487 selectEdge: function(edge){
488 if(this.selectedEdge == edge){
491 if(this.selectedEdge){
492 this.selectedEdge.isSelected = false;
495 edge.isSelected = true;
497 this.selectedEdge = edge;
498 if(this.callback_selectedEdgeChanged){
499 this.callback_selectedEdgeChanged(this.selectedEdge);
502 setIdentifierForSelectedNode: function(str, destNode){
504 if(this.selectedNode){
505 if(this.srcMemoryDB){
506 this.srcMemoryDB.updateNode(str, this.selectedNode.typeid, this.selectedNode.nodeid);
508 this.selectedNode.identifier = str;
512 if(this.selectedNodeDestination){
513 if(this.srcMemoryDB){
514 this.srcMemoryDB.updateNode(str, this.selectedNodeDestination.typeid, this.selectedNodeDestination.nodeid);
516 this.selectedNodeDestination.identifier = str;
523 function MGNode(env, identifier){
526 this.identifier = identifier;
528 this.nodeid = undefined;
529 this.typeid = undefined;
531 this.position = new Point2D(Math.random() * 32 - 16, Math.random() * 32 - 16);
534 this.vector = new Point2D(Math.random() * 2 - 1, Math.random() * 2 - 1);
535 this.friction = 50 / 100;
536 this.repulsionLengthNode = 90;
537 this.repulsionLengthEdge = 90;
538 this.ignoreEdgeRepulsion = 0;
540 this.strokeStyle = "rgba(0, 0, 0, 1)";
541 this.isSelected = false;
546 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
548 this.env.context.strokeStyle = this.strokeStyle;
550 this.env.drawCircle(this.position.x, this.position.y, this.size);
552 this.env.drawText(this.identifier.toString(), this.position.x + 10, this.position.y + 10);
560 this.position.x += this.vector.x;
561 this.position.y += this.vector.y;
562 this.vector.x *= this.friction;
563 this.vector.y *= this.friction;
565 if(!this.ignoreEdgeRepulsion){
567 //距離の近い点同士には斥力が働くとする。
568 p = this.env.nodeList;
569 for(var i = 0, iLen = p.length; i < iLen; i++){
570 var q = this.env.nodeList[i];
572 l = this.env.getVectorLengthP(this.position, q.position);
573 if(l < this.repulsionLengthNode && l != 0){
574 e = this.env.getUnitVectorP(q.position, this.position);
575 e.x *= this.repulsionLengthNode / l;
576 e.y *= this.repulsionLengthNode / l;
577 this.vector.x += e.x;
578 this.vector.y += e.y;
584 //自分を端点に含まないエッジとの距離が近い場合、そのエッジから遠ざかろうとする。
586 p = this.env.edgeList;
587 for(var i = 0, iLen = p.length; i < iLen; i++){
588 var q = this.env.edgeList[i];
589 if(q.node0 != this && q.node1 != this){
590 l = this.env.getDistanceDotAndLineP(this.position, q.node0.position, q.node1.position);
591 if(l < this.repulsionLengthEdge && l != 0){
595 e = this.env.getNormalUnitVectorSideOfP(q.node0.position, q.node1.position, this.position);
596 e.x *= this.repulsionLengthEdge / l;
597 e.y *= this.repulsionLengthEdge / l;
598 this.vector.x += e.x;
599 this.vector.y += e.y;
600 q.node0.vector.x -= e.x / 2;
601 q.node0.vector.y -= e.y / 2;
602 q.node1.vector.x -= e.x / 2;
603 q.node1.vector.y -= e.y / 2;
609 this.ignoreEdgeRepulsion--;
615 function MGEdge(env, identifier, node0, node1){
618 this.identifier = identifier;
625 this.freeLength = 250;
627 this.strokeStyle = "rgba(0, 0, 0, 0.5)";
628 this.isSelected = false;
630 this.centerPoint = new Point2D(0, 0);
635 this.env.context.strokeStyle = "rgba(255, 0, 0, 1)";
637 this.env.context.strokeStyle = this.strokeStyle;
639 if(this.node0 && this.node1){
640 //this.drawCurvedLineP(this.node0.position, this.node1.position);
641 this.env.drawLineP(this.node0.position, this.node1.position)
642 //this.env.strokeRect(this.centerPoint.x - 8, this.centerPoint.y - 8, 16, 16);
644 this.env.drawText(this.identifier.toString(), this.centerPoint.x, this.centerPoint.y);
649 var l = this.env.getVectorLengthP(this.node0.position, this.node1.position);
651 if(l > this.freeLength){
652 e = this.env.getUnitVectorP(this.node0.position, this.node1.position);
653 e.x *= l / this.freeLength;
654 e.y *= l / this.freeLength;
655 this.node0.vector.x += e.x;
656 this.node0.vector.y += e.y;
657 this.node1.vector.x -= e.x;
658 this.node1.vector.y -= e.y;
660 this.centerPoint = new Point2D((this.node0.position.x + this.node1.position.x) / 2, (this.node0.position.y + this.node1.position.y) / 2);
662 drawCurvedLineP: function(p, q){
664 var d = function(x){ return that.env.drawCoordinatesInInteger(x); };
665 var v = this.env.getUnitVectorP(p, q);
666 var w = new Point2D(-(v.y * 50), v.x * 50);
667 this.env.context.beginPath();
668 this.env.context.moveTo(d(p.x), d(p.y));
669 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));
670 //this.env.context.closePath();
671 this.env.context.stroke();
675 function Point2D(x, y){
679 Point2D.prototype = {
683 function Rectangle(x, y, width, height){
684 this.origin = new Point2D(x,y);
685 this.size = new Point2D(width,height);
687 Rectangle.prototype = {
688 isIncludePointP: function(p){
689 return (this.origin.x <= p.x) && (p.x <= this.origin.x + this.size.x) &&
690 (this.origin.y <= p.y) && (p.y <= this.origin.y + this.size.y);