Team:Evry/arbor/src/physics/system.js

From 2012.igem.org

Revision as of 18:04, 18 September 2012 by TriCer (Talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

// // system.js // // the main controller object for creating/modifying graphs //

 var ParticleSystem = function(repulsion, stiffness, friction, centerGravity, targetFps, dt, precision){
 // also callable with ({stiffness:, repulsion:, friction:, timestep:, fps:, dt:, gravity:})
   
   var _changes=[]
   var _notification=null
   var _epoch = 0
   var _screenSize = null
   var _screenStep = .04
   var _screenPadding = [20,20,20,20]
   var _bounds = null
   var _boundsTarget = null
   if (typeof stiffness=='object'){
     var _p = stiffness
     friction = _p.friction
     repulsion = _p.repulsion
     targetFps = _p.fps
     dt = _p.dt
     stiffness = _p.stiffness
     centerGravity = _p.gravity
     precision = _p.precision
   }
   friction = isNaN(friction) ? .5 : friction
   repulsion = isNaN(repulsion) ? 1000 : repulsion
   targetFps = isNaN(targetFps) ? 55 : targetFps
   stiffness = isNaN(stiffness) ? 600 : stiffness
   dt = isNaN(dt) ? 0.02 : dt
   precision = isNaN(precision) ? .6 : precision
   centerGravity = (centerGravity===true)
   var _systemTimeout = (targetFps!==undefined) ? 1000/targetFps : 1000/50
   var _parameters = {repulsion:repulsion, stiffness:stiffness, friction:friction, dt:dt, gravity:centerGravity, precision:precision, timeout:_systemTimeout}
   var _energy
   var state = {
     renderer:null, // this is set by the library user
     tween:null, // gets filled in by the Kernel
     nodes:{}, // lookup based on node _id's from the worker
     edges:{}, // likewise
     adjacency:{}, // {name1:{name2:{}, name3:{}}}
     names:{}, // lookup table based on 'name' field in data objects
     kernel: null
   }
   var that={
     parameters:function(newParams){
       if (newParams!==undefined){
         if (!isNaN(newParams.precision)){
           newParams.precision = Math.max(0, Math.min(1, newParams.precision))
         }
         $.each(_parameters, function(p, v){
           if (newParams[p]!==undefined) _parameters[p] = newParams[p]
         })
         state.kernel.physicsModified(newParams)
       }
       return _parameters
     },
     fps:function(newFPS){
       if (newFPS===undefined) return state.kernel.fps()
       else that.parameters({timeout:1000/(newFPS||50)})
     },


     start:function(){
       state.kernel.start()
     },
     stop:function(){
       state.kernel.stop()
     },
     addNode:function(name, data){
       data = data || {}
       var priorNode = state.names[name]
       if (priorNode){
         priorNode.data = data
         return priorNode
       }else if (name!=undefined){
         // the data object has a few magic fields that are actually used
         // by the simulation:
         //   'mass' overrides the default of 1
         //   'fixed' overrides the default of false
         //   'x' & 'y' will set a starting position rather than 
         //             defaulting to random placement
         var x = (data.x!=undefined) ? data.x : null
         var y = (data.y!=undefined) ? data.y : null
         var fixed = (data.fixed) ? 1 : 0
         var node = new Node(data)
         node.name = name
         state.names[name] = node
         state.nodes[node._id] = node;
         _changes.push({t:"addNode", id:node._id, m:node.mass, x:x, y:y, f:fixed})
         that._notify();
         return node;
       }
     },
     // remove a node and its associated edges from the graph
     pruneNode:function(nodeOrName) {
       var node = that.getNode(nodeOrName)
       
       if (typeof(state.nodes[node._id]) !== 'undefined'){
         delete state.nodes[node._id]
         delete state.names[node.name]
       }


       $.each(state.edges, function(id, e){
         if (e.source._id === node._id || e.target._id === node._id){
           that.pruneEdge(e);
         }
       })
       _changes.push({t:"dropNode", id:node._id})
       that._notify();
     },
     getNode:function(nodeOrName){
       if (nodeOrName._id!==undefined){
         return nodeOrName
       }else if (typeof nodeOrName=='string' || typeof nodeOrName=='number'){
         return state.names[nodeOrName]
       }
       // otherwise let it return undefined
     },
     eachNode:function(callback){
       // callback should accept two arguments: Node, Point
       $.each(state.nodes, function(id, n){
         if (n._p.x==null || n._p.y==null) return
         var pt = (_screenSize!==null) ? that.toScreen(n._p) : n._p
         callback.call(that, n, pt);
       })
     },
     addEdge:function(source, target, data){
       source = that.getNode(source) || that.addNode(source)
       target = that.getNode(target) || that.addNode(target)
       data = data || {}
       var edge = new Edge(source, target, data);
       var src = source._id
       var dst = target._id
       state.adjacency[src] = state.adjacency[src] || {}
       state.adjacency[src][dst] = state.adjacency[src][dst] || []
       var exists = (state.adjacency[src][dst].length > 0)
       if (exists){
         // probably shouldn't allow multiple edges in same direction
         // between same nodes? for now just overwriting the data...
         $.extend(state.adjacency[src][dst].data, edge.data)
         return
       }else{
         state.edges[edge._id] = edge
         state.adjacency[src][dst].push(edge)
         var len = (edge.length!==undefined) ? edge.length : 1
         _changes.push({t:"addSpring", id:edge._id, fm:src, to:dst, l:len})
         that._notify()
       }
       return edge;
     },
     // remove an edge and its associated lookup entries
     pruneEdge:function(edge) {
       _changes.push({t:"dropSpring", id:edge._id})
       delete state.edges[edge._id]
       
       for (var x in state.adjacency){
         for (var y in state.adjacency[x]){
           var edges = state.adjacency[x][y];
           for (var j=edges.length - 1; j>=0; j--)  {
             if (state.adjacency[x][y][j]._id === edge._id){
               state.adjacency[x][y].splice(j, 1);
             }
           }
         }
       }
       that._notify();
     },
     // find the edges from node1 to node2
     getEdges:function(node1, node2) {
       node1 = that.getNode(node1)
       node2 = that.getNode(node2)
       if (!node1 || !node2) return []
       
       if (typeof(state.adjacency[node1._id]) !== 'undefined'
         && typeof(state.adjacency[node1._id][node2._id]) !== 'undefined'){
         return state.adjacency[node1._id][node2._id];
       }
       return [];
     },
     getEdgesFrom:function(node) {
       node = that.getNode(node)
       if (!node) return []
       
       if (typeof(state.adjacency[node._id]) !== 'undefined'){
         var nodeEdges = []
         $.each(state.adjacency[node._id], function(id, subEdges){
           nodeEdges = nodeEdges.concat(subEdges)
         })
         return nodeEdges
       }
       return [];
     },
     getEdgesTo:function(node) {
       node = that.getNode(node)
       if (!node) return []
       var nodeEdges = []
       $.each(state.edges, function(edgeId, edge){
         if (edge.target == node) nodeEdges.push(edge)
       })
       
       return nodeEdges;
     },
     eachEdge:function(callback){
       // callback should accept two arguments: Edge, Point
       $.each(state.edges, function(id, e){
         var p1 = state.nodes[e.source._id]._p
         var p2 = state.nodes[e.target._id]._p


         if (p1.x==null || p2.x==null) return
         
         p1 = (_screenSize!==null) ? that.toScreen(p1) : p1
         p2 = (_screenSize!==null) ? that.toScreen(p2) : p2
         
         if (p1 && p2) callback.call(that, e, p1, p2);
       })
     },


     prune:function(callback){
       // callback should be of the form Æ’(node, {from:[],to:[]})
       var changes = {dropped:{nodes:[], edges:[]}}
       if (callback===undefined){
         $.each(state.nodes, function(id, node){
           changes.dropped.nodes.push(node)
           that.pruneNode(node)
         })
       }else{
         that.eachNode(function(node){
           var drop = callback.call(that, node, {from:that.getEdgesFrom(node), to:that.getEdgesTo(node)})
           if (drop){
             changes.dropped.nodes.push(node)
             that.pruneNode(node)
           }
         })
       }
       // trace('prune', changes.dropped)
       return changes
     },
     
     graft:function(branch){
       // branch is of the form: { nodes:{name1:{d}, name2:{d},...}, 
       //                          edges:{fromNm:{toNm1:{d}, toNm2:{d}}, ...} }
       var changes = {added:{nodes:[], edges:[]}}
       if (branch.nodes) $.each(branch.nodes, function(name, nodeData){
         var oldNode = that.getNode(name)
         // should probably merge any x/y/m data as well...
         // if (oldNode) $.extend(oldNode.data, nodeData)
         
         if (oldNode) oldNode.data = nodeData
         else changes.added.nodes.push( that.addNode(name, nodeData) )
         
         state.kernel.start()
       })
       
       if (branch.edges) $.each(branch.edges, function(src, dsts){
         var srcNode = that.getNode(src)
         if (!srcNode) changes.added.nodes.push( that.addNode(src, {}) )
         $.each(dsts, function(dst, edgeData){
           // should probably merge any x/y/m data as well...
           // if (srcNode) $.extend(srcNode.data, nodeData)


           // i wonder if it should spawn any non-existant nodes that are part
           // of one of these edge requests...
           var dstNode = that.getNode(dst)
           if (!dstNode) changes.added.nodes.push( that.addNode(dst, {}) )
           var oldEdges = that.getEdges(src, dst)
           if (oldEdges.length>0){
             // trace("update",src,dst)
             oldEdges[0].data = edgeData
           }else{
           // trace("new ->",src,dst)
             changes.added.edges.push( that.addEdge(src, dst, edgeData) )
           }
         })
       })
       // trace('graft', changes.added)
       return changes
     },
     merge:function(branch){
       var changes = {added:{nodes:[], edges:[]}, dropped:{nodes:[], edges:[]}}
       $.each(state.edges, function(id, edge){
         // if ((branch.edges[edge.source.name]===undefined || branch.edges[edge.source.name][edge.target.name]===undefined) &&
         //     (branch.edges[edge.target.name]===undefined || branch.edges[edge.target.name][edge.source.name]===undefined)){
         if ((branch.edges[edge.source.name]===undefined || branch.edges[edge.source.name][edge.target.name]===undefined)){
               that.pruneEdge(edge)
               changes.dropped.edges.push(edge)
             }
       })
       
       var prune_changes = that.prune(function(node, edges){
         if (branch.nodes[node.name] === undefined){
           changes.dropped.nodes.push(node)
           return true
         }
       })
       var graft_changes = that.graft(branch)        
       changes.added.nodes = changes.added.nodes.concat(graft_changes.added.nodes)
       changes.added.edges = changes.added.edges.concat(graft_changes.added.edges)
       changes.dropped.nodes = changes.dropped.nodes.concat(prune_changes.dropped.nodes)
       changes.dropped.edges = changes.dropped.edges.concat(prune_changes.dropped.edges)
       
       // trace('changes', changes)
       return changes
     },


     tweenNode:function(nodeOrName, dur, to){
       var node = that.getNode(nodeOrName)
       if (node) state.tween.to(node, dur, to)
     },
     tweenEdge:function(a,b,c,d){
       if (d===undefined){
         // called with (edge, dur, to)
         that._tweenEdge(a,b,c)
       }else{
         // called with (node1, node2, dur, to)
         var edges = that.getEdges(a,b)
         $.each(edges, function(i, edge){
           that._tweenEdge(edge, c, d)    
         })
       }
     },
     _tweenEdge:function(edge, dur, to){
       if (edge && edge._id!==undefined) state.tween.to(edge, dur, to)
     },
     _updateGeometry:function(e){
       if (e != undefined){          
         var stale = (e.epoch<_epoch)
         _energy = e.energy
         var pts = e.geometry // an array of the form [id1,x1,y1, id2,x2,y2, ...]
         if (pts!==undefined){
           for (var i=0, j=pts.length/3; i<j; i++){
             var id = pts[3*i]
                           
             // canary silencer...
             if (stale && state.nodes[id]==undefined) continue
             
             state.nodes[id]._p.x = pts[3*i + 1]
             state.nodes[id]._p.y = pts[3*i + 2]
           }
         }          
       }
     },
     
     // convert to/from screen coordinates
     screen:function(opts){
       if (opts == undefined) return {size:(_screenSize)? objcopy(_screenSize) : undefined, 
                                      padding:_screenPadding.concat(), 
                                      step:_screenStep}
       if (opts.size!==undefined) that.screenSize(opts.size.width, opts.size.height)
       if (!isNaN(opts.step)) that.screenStep(opts.step)
       if (opts.padding!==undefined) that.screenPadding(opts.padding)
     },
     
     screenSize:function(canvasWidth, canvasHeight){
       _screenSize = {width:canvasWidth,height:canvasHeight}
       that._updateBounds()
     },
     screenPadding:function(t,r,b,l){
       if ($.isArray(t)) trbl = t
       else trbl = [t,r,b,l]
       var top = trbl[0]
       var right = trbl[1]
       var bot = trbl[2]
       if (right===undefined) trbl = [top,top,top,top]
       else if (bot==undefined) trbl = [top,right,top,right]
       
       _screenPadding = trbl
     },
     screenStep:function(stepsize){
       _screenStep = stepsize
     },
     toScreen:function(p) {
       if (!_bounds || !_screenSize) return
       // trace(p.x, p.y)
       var _padding = _screenPadding || [0,0,0,0]
       var size = _bounds.bottomright.subtract(_bounds.topleft)
       var sx = _padding[3] + p.subtract(_bounds.topleft).divide(size.x).x * (_screenSize.width - (_padding[1] + _padding[3]))
       var sy = _padding[0] + p.subtract(_bounds.topleft).divide(size.y).y * (_screenSize.height - (_padding[0] + _padding[2]))
       // return arbor.Point(Math.floor(sx), Math.floor(sy))
       return arbor.Point(sx, sy)
     },
     
     fromScreen:function(s) {
       if (!_bounds || !_screenSize) return
       var _padding = _screenPadding || [0,0,0,0]
       var size = _bounds.bottomright.subtract(_bounds.topleft)
       var px = (s.x-_padding[3]) / (_screenSize.width-(_padding[1]+_padding[3]))  * size.x + _bounds.topleft.x
       var py = (s.y-_padding[0]) / (_screenSize.height-(_padding[0]+_padding[2])) * size.y + _bounds.topleft.y
       return arbor.Point(px, py);
     },
     _updateBounds:function(newBounds){
       // step the renderer's current bounding box closer to the true box containing all
       // the nodes. if _screenStep is set to 1 there will be no lag. if _screenStep is
       // set to 0 the bounding box will remain stationary after being initially set 
       if (_screenSize===null) return
       
       if (newBounds) _boundsTarget = newBounds
       else _boundsTarget = that.bounds()
       
       // _boundsTarget = newBounds || that.bounds()
       // _boundsTarget.topleft = new Point(_boundsTarget.topleft.x,_boundsTarget.topleft.y)
       // _boundsTarget.bottomright = new Point(_boundsTarget.bottomright.x,_boundsTarget.bottomright.y)
       var bottomright = new Point(_boundsTarget.bottomright.x, _boundsTarget.bottomright.y)
       var topleft = new Point(_boundsTarget.topleft.x, _boundsTarget.topleft.y)
       var dims = bottomright.subtract(topleft)
       var center = topleft.add(dims.divide(2))


       var MINSIZE = 4                                   // perfect-fit scaling
       // MINSIZE = Math.max(Math.max(MINSIZE,dims.y), dims.x) // proportional scaling
       var size = new Point(Math.max(dims.x,MINSIZE), Math.max(dims.y,MINSIZE))
       _boundsTarget.topleft = center.subtract(size.divide(2))
       _boundsTarget.bottomright = center.add(size.divide(2))
       if (!_bounds){
         if ($.isEmptyObject(state.nodes)) return false
         _bounds = _boundsTarget
         return true
       }
       
       // var stepSize = (Math.max(dims.x,dims.y)<MINSIZE) ? .2 : _screenStep
       var stepSize = _screenStep
       _newBounds = {
         bottomright: _bounds.bottomright.add( _boundsTarget.bottomright.subtract(_bounds.bottomright).multiply(stepSize) ),
         topleft: _bounds.topleft.add( _boundsTarget.topleft.subtract(_bounds.topleft).multiply(stepSize) )
       }
       
       // return true if we're still approaching the target, false if we're ‘close enough’
       var diff = new Point(_bounds.topleft.subtract(_newBounds.topleft).magnitude(), _bounds.bottomright.subtract(_newBounds.bottomright).magnitude())        
       if (diff.x*_screenSize.width>1 || diff.y*_screenSize.height>1){
         _bounds = _newBounds
         return true
       }else{
        return false        
       }
     },
     energy:function(){
       return _energy
     },
     bounds:function(){
       //  TL   -1
       //     -1   1
       //        1   BR
       var bottomright = null
       var topleft = null
       // find the true x/y range of the nodes
       $.each(state.nodes, function(id, node){
         if (!bottomright){
           bottomright = new Point(node._p)
           topleft = new Point(node._p)
           return
         }
       
         var point = node._p
         if (point.x===null || point.y===null) return
         if (point.x > bottomright.x) bottomright.x = point.x;
         if (point.y > bottomright.y) bottomright.y = point.y;          
         if   (point.x < topleft.x)   topleft.x = point.x;
         if   (point.y < topleft.y)   topleft.y = point.y;
       })


       // return the true range then let to/fromScreen handle the padding
       if (bottomright && topleft){
         return {bottomright: bottomright, topleft: topleft}
       }else{
         return {topleft: new Point(-1,-1), bottomright: new Point(1,1)};
       }
     },
     // Find the nearest node to a particular position
     nearest:function(pos){
       if (_screenSize!==null) pos = that.fromScreen(pos)
       // if screen size has been specified, presume pos is in screen pixel
       // units and convert it back to the particle system coordinates
       
       var min = {node: null, point: null, distance: null};
       var t = that;
       
       $.each(state.nodes, function(id, node){
         var pt = node._p
         if (pt.x===null || pt.y===null) return
         var distance = pt.subtract(pos).magnitude();
         if (min.distance === null || distance < min.distance){
           min = {node: node, point: pt, distance: distance};
           if (_screenSize!==null) min.screenPoint = that.toScreen(pt)
         }
       })
       
       if (min.node){
         if (_screenSize!==null) min.distance = that.toScreen(min.node.p).subtract(that.toScreen(pos)).magnitude()
          return min
       }else{
          return null
       }
     },
     _notify:function() {
       // pass on graph changes to the physics object in the worker thread
       // (using a short timeout to batch changes)
       if (_notification===null) _epoch++
       else clearTimeout(_notification)
       
       _notification = setTimeout(that._synchronize,20)
       // that._synchronize()
     },
     _synchronize:function(){
       if (_changes.length>0){
         state.kernel.graphChanged(_changes)
         _changes = []
         _notification = null
       }
     },
   }    
   
   state.kernel = Kernel(that)
   state.tween = state.kernel.tween || null
   
   // some magic attrs to make the Node objects phone-home their physics-relevant changes
   Node.prototype.__defineGetter__("p", function() { 
     var self = this
     var roboPoint = {}
     roboPoint.__defineGetter__('x', function(){ return self._p.x; })
     roboPoint.__defineSetter__('x', function(newX){ state.kernel.particleModified(self._id, {x:newX}) })
     roboPoint.__defineGetter__('y', function(){ return self._p.y; })
     roboPoint.__defineSetter__('y', function(newY){ state.kernel.particleModified(self._id, {y:newY}) })
     roboPoint.__proto__ = Point.prototype
     return roboPoint
   })
   Node.prototype.__defineSetter__("p", function(newP) { 
     this._p.x = newP.x
     this._p.y = newP.y
     state.kernel.particleModified(this._id, {x:newP.x, y:newP.y})
   })
   Node.prototype.__defineGetter__("mass", function() { return this._mass; });
   Node.prototype.__defineSetter__("mass", function(newM) { 
     this._mass = newM
     state.kernel.particleModified(this._id, {m:newM})
   })
   Node.prototype.__defineSetter__("tempMass", function(newM) { 
     state.kernel.particleModified(this._id, {_m:newM})
   })
     
   Node.prototype.__defineGetter__("fixed", function() { return this._fixed; });
   Node.prototype.__defineSetter__("fixed", function(isFixed) { 
     this._fixed = isFixed
     state.kernel.particleModified(this._id, {f:isFixed?1:0})
   })
   
   return that
 }