// Simulation extension for phyiscal (behaviorual) agents var RTree = Require('rtree/rtree'); var RBush = Require('rtree/rbush'); var RBushKnn = Require('rtree/rbush-knn'); var Comp = Require('com/compat'); var current=none; var Aios = none; // patchgrid agent instance counter var instances = 0; // Geometric Utiliy Functions function sind(x) { return Math.sin(x/360*(2*Math.PI)) } function cosd(x) { return Math.cos(x/360*(2*Math.PI)) } function rotate(d,a) { return [ int(d[0]*cosd(a)-d[1]*sind(a)), int(d[1]*sind(a)+d[0]*sind(a)) ] } function distance2Rect (pos,bbox,scale) { if (!scale) scale={x:1,y:1}; var px = pos.x, py = pos.y, x0 = bbox.x+bbox.w/2, y0 = bbox.y+bbox.h/2, dx = (Math.max(Math.abs(px - x0) - bbox.w / 2, 0))/scale.x, dy = (Math.max(Math.abs(py - y0) - bbox.h / 2, 0))/scale.y; return Math.sqrt(Math.pow(dx,2)+Math.pow(dy,2)) } function distance (pos1,pos2,scale) { if (!scale) scale={x:1,y:1}; var dx = Math.abs(pos1.x - pos2.x) / scale.x, dy = Math.abs(pos1.y - pos2.y) / scale.y; return Math.sqrt(Math.pow(dx,2)+Math.pow(dy,2)) } /* construct bbox {x,y,w,h} from geometric data {x,y,x0,y0,x1,y1,dx,dy,w,h,dir} relative to current position {x,y} optional bounds {x0,y0,x1,y1} +-----> x | N x,y--+ | W X E | | v S +--w,h y */ function makeBbox (pos,geo,bounds) { bbox={x:pos.x,y:pos.y,w:0,h:0} // {x,y,w,h} if (typeof geo == 'number') // radius around center pos return {x:pos.x-geo,y:pos.y-geo,w:2*geo+1,h:2*geo+1}; if (geo.x) bbox.x=geo.x; if (geo.y) bbox.y=geo.y; if (geo.x0) bbox.x=geo.x0; if (geo.y0) bbox.x=geo.y0; if (geo.dx) bbox.x=pos.x+geo.dx; if (geo.dy) bbox.y=pos.y+geo.dy; if (geo.w) bbox.w=geo.w; if (geo.h) bbox.w=geo.h; if (geo.x1) bbox.w=geo.x1-bbox.x+1; if (geo.y1) bbox.h=geo.y1-bbox.y+1; if (geo.r) return {x:bbox.x-geo.r,y:bbox.y-geo.r,w:2*geo.r+1,h:2*geo.r+1}; if (geo.dir) switch (geo.dir) { // including current position X // Ex. WEST: // **** // ***X // **** case Aios.DIR.NORTH: if (geo.distance) bbox.w=geo.spread||1,bbox.h=geo.distance+1; bbox.x -= int(bbox.w/2); bbox.y -= (bbox.h-1); break; case Aios.DIR.SOUTH: if (geo.distance) bbox.w=geo.spread||1,bbox.h=geo.distance+1; bbox.x -= int(bbox.w/2); break; case Aios.DIR.WEST: if (geo.distance) bbox.h=geo.spread||1,bbox.w=geo.distance+1; bbox.y -= int(bbox.h/2); bbox.x -= (bbox.w-1); break; case Aios.DIR.EAST: if (geo.distance) bbox.h=geo.spread||1,bbox.w=geo.distance+1; bbox.y -= int(bbox.h/2); break; } return bbox; } function bbox2pp(bbox) { return {x0:bbox.x,y0:bbox.y,x1:bbox.x+bbox.w-1,y1:bbox.y+bbox.h-1, dir:bbox.dir,distance:bbox.distance} } function pp2bbox(pp) { return {x:pp.x0,y:pp.y0,w:pp.x1-pp.x0+1,h:pp.y1-pp.y0+1} } function whatType(what) { // agent-twin => agent var tokens = what.match(/([a-z]+)(-.+)/) return tokens?tokens[1]:what; } function whatName(what) { // agent-twin => twin var tokens = what.match(/[a-z]+-(.+)/) return tokens?tokens[1]:null; } /* ** Generic simulation object iterator (NetLogo comp.) ** 1. Agents ** ask('agent','*',cb) // all ** ask('agent',null,cb) // here ** ask('agent',dir,cb) ** ask('agent',bbox,cb) ** ask('agents-class','*',cb) ** ask('agent',id:string,cb) ** ask('agent',[id1:string,id2,..],cb) ** ask('agent',5,cb) == in-radius 5 ** ask('agent',[x,y],cb) == @(x,y) ** ask('agent',{x,y,w,h},cb?) == within[x,y,x+w,y+h] ** ask('agent',{x0,y0,x1,y1},cb?) == within[x,y,x+w,y+h] ** ask('agent',{dx,dy,w,h},cb?) == within[dx+x0,dy+y0,x0+dx+w,y0+dy+h] ** ask('agent',{x,y,w,h},cb?) == within[x,y-x+w,y+h] ** => returns only physical agents! ** ** 2. Resources ** ask('resource',..) ** ask('resources-class',..) ** ** 3. Patches ** ask('patch',..) ** ask('patches',..) ** ** Groups: ** ask('children') ** ask('parent') ** ** ask('distance',dir|number|bbox) ** ** Note: callback is executed in CURRENT agent context unless remote flag is set True: ** typeof callback = function (agent object,node object) ** ** TODO: check for consitency when using arrow callback functions (no this rebind possible) */ function aiosXnet(aiosXsimu,module) { var self=this; Aios=module;current=Aios.current; function ask(what,who,callback,remote) { var type = self.gui.classObject(what)||what, node = current.node,nodeId, pos = node.position, bbox, jump, desc,id,pro,set=[],set2=[],set3=[],multiple=true, i,j,r,row,col,p,q,agent,agents,nodes,pro, name=what.match(/[a-z]+-(.+)/,''); if (!self.options.patch) return; if (name) name=name[1]; if (what.indexOf(type+'s')==0) type += 's'; switch (type) { case 'agent': multiple=false; case 'agents': if (typeof who == 'string') { // entire world has to be searched for agent if (who=='*') who=/.+/; else if (self.cache.agent2node[who]) { agent=self.cache.agent2node[who].getAgentProcess(0); set=[{ agent:agent.agent.id, class:agent.agent.ac, pos:agent.node.position, distance:distance(pos,agent.node.position), obj:agent.agent }] set2=[agent.node] } if (set.length==0) { agents=self.world.getAgentProcess(who,name); if (agents) { if (Comp.obj.isArray(agents)) { set=agents.map(function (ap) { return { agent:ap.agent.id, class:ap.agent.ac, pos:ap.node.position, distance:distance(pos,ap.node.position), obj:ap.agent } }); set2=agents.map(function (ap) { return ap.node }); } else { agent=agents; self.cache.agent2node[agent.agent.id]=agent.node; node = agent.node; set.push({ agent:agent.agent.id, class:agent.agent.ac, pos:agent.node.position, distance:distance(pos,agent.node.position), obj:agent.agent }); set2.push(node); if (remote) set3.push(agent); } } } } else if ((Comp.obj.isArray(who) && who.length==2) || who==null) { if (!who) who=[pos.x,pos.y]; if (self.agentMap) { if (!self.checkBounds(who[0],who[1])) return []; row=self.agentMap[who[1]]; col=row[who[0]]; for(p in col) { agent=self.getAgentProcess(col[p],p); if (agent && (!name || agent.agent.ac==name)) { set.push({ agent:agent.agent.id, class:agent.agent.ac, pos:agent.node.position, distance:distance(pos,agent.node.position), obj:agent.agent }); set2.push(agent.node); if (remote) set3.push(agent); } } } } else if (typeof who == 'number' || Comp.obj.isObj(who)) { // Bounding box {}? bbox=bbox2pp(makeBbox(pos,who)); // find agents on nodes in the neighbourhood if (self.agentMap) { for(i=bbox.x0;i<=bbox.x1;i++) for(j=bbox.y0;j<=bbox.y1;j++) { if (!self.checkBounds(i,j)) continue; row=self.agentMap[j]; col=row[i]; for(p in col) { agent=self.getAgentProcess(col[p],p); if (agent && (!name || agent.agent.ac==name)) { set.push({ agent:agent.agent.id, class:agent.agent.ac, pos:agent.node.position, distance:distance(pos,agent.node.position), obj:agent.agent }); set2.push(agent.node); if (remote) set3.push(agent); } } } } else { if (!name) return; self.world.nodes.forEach(function (_node) { if (wihtin(_node.position,node.position,who)) { agents=_node.getAgentProcess(/[a-zA-Z]+/); for(p in agents) { if (agents[p].agent.ac==name) { set.push({ agent:agents[p].agent.id, class:agents[p].agent.ac, pos:_node.position, distance:distance(pos,_node.position), obj:agents[p].agent }); set2.push(_node); } } } }) } } break; case 'resource': multiple=false; case 'resources': var px = pos.x * self.options.patch.width, py = pos.y * self.options.patch.height; if (typeof who == 'string' && who.indexOf('DIR.')<0 && who != '*') { var obj = self.gui.objects['resource['+who+']']; if (obj) set = {resource:who, class:obj.class, data:obj.data, distance:distance2Rect({x:px,y:py}, obj.visual, {x:self.options.patch.width, y:self.options.patch.height}), x:int(obj.visual.x/self.options.patch.width), y:int(obj.visual.y/self.options.patch.height), w:int(obj.visual.w/self.options.patch.width), h:int(obj.visual.h/self.options.patch.height) } } else if (typeof who == 'number' || Comp.obj.isObj(who) || who=='*' || (Comp.obj.isArray(who) && who.length==2) || who==null) { if (!who) who=[pos.x,pos.y]; if (Comp.obj.isArray(who)) bbox={x:who[0],y:who[1],w:1,h:1}; // or w/h=0 else if (who=='*') bbox={x:0,y:0,w:self.options.x,h:self.options.y}; else bbox=makeBbox(pos,who); set=self.rtree.search(bbox).map(function (rid) { var obj = self.gui.objects[rid]; if (name && obj.class != name) return; return {resource:rid.replace(/resource\[([^\]]+)\]/,'$1'), class:obj.class, data:obj.data, distance:distance2Rect({x:px,y:py}, obj.visual, {x:self.options.patch.width, y:self.options.patch.height}), x:int(obj.visual.x/self.options.patch.width), y:int(obj.visual.y/self.options.patch.height), w:int(obj.visual.w/self.options.patch.width), h:int(obj.visual.h/self.options.patch.height) } }).filter (function (o) {return o}); } break; case 'patch': multiple=false; case 'patches': if ((Comp.obj.isArray(who) && who.length==2) || who==null) { if (!who) who=[pos.x,pos.y]; if (!self.checkBounds(who[0],who[1])) return []; set=self.patches[who[1]][who[0]]; } else if (who=='*') { set=self.patches; } else if (typeof who == 'number' || Comp.obj.isObj(who)) { // Bounding box? bbox=bbox2pp(makeBbox(pos,who)); // find patches in the neighbourhood if (name == 'array') { for(j=bbox.y0;j<=bbox.y1;j++) { for(i=bbox.x0;i<=bbox.x1;i++) { if (!self.checkBounds(i,j)) continue; set.push(self.patches[j][i]) } } } else for(j=bbox.y0;j<=bbox.y1;j++) { row=[] for(i=bbox.x0;i<=bbox.x1;i++) { if (!self.checkBounds(i,j)) continue; row.push(self.patches[j][i]) } if (row.length==1) set.push(row[0]) else set.push(row) } } break; // groups case 'parent': if (node.parent) { agents = node.parent.getAgent(/.+/); set=agents && agents[0]?agents[0].id:undefined; } break; case 'children': multiple=true; if (node.children) { agents=[] node.children.forEach(function (child) { agents = agents.concat(child.getAgent(/.+/)); }) set=agents; } break; case 'distance': function lookup(x,y) { var row,col,p,a=[]; if (jump && jump.x == x && jump.y == y) return; row=self.agentMap[y]; if (!row) return; col=row[x]; if (!col) return; for(p in col) { a.push(self.getNode(col[p])); } return a.length?a:null; } name=name||''; if ((typeof who == 'string' && who.indexOf('DIR.')==0) || typeof who == 'number' || Comp.obj.isObj(who)) { bbox={x:pos.x,y:pos.y} if (typeof who == 'number') bbox.distance=who; if (typeof who == 'string') bbox.dir=who; if (typeof who == 'object') bbox.dir=who.dir,bbox.distance=who.distance; agents=null; switch (bbox.dir) { case Aios.DIR.NORTH: bbox={y1:bbox.y-1,y0:bbox.distance?(bbox.y-bbox.distance):0, x0:pos.x,x1:pos.x,dir:bbox.dir}; break; case Aios.DIR.SOUTH: bbox={y0:bbox.y+1,y1:bbox.distance?(bbox.y+bbox.distance):self.options.y-1, x0:pos.x,x1:pos.x,dir:bbox.dir}; break; case Aios.DIR.WEST: bbox={x1:bbox.x-1,x0:bbox.distance?(bbox.x+bbox.distance):0, y0:pos.y,y1:pos.y,dir:bbox.dir}; break; case Aios.DIR.EAST: bbox={x0:bbox.x+1,x1:bbox.distance?(bbox.x+bbox.distance):self.options.x-1, y0:pos.y,y1:pos.y,dir:bbox.dir}; break; } if (name.indexOf('resource')<0) { // 1. Agents // Jump over attached group children if (node.children) { jump=node.children[0].position; } switch (bbox.dir) { case Aios.DIR.NORTH: for(i=bbox.y1;i>=bbox.y0 && !nodes;i--) nodes=lookup(pos.x,i); break; case Aios.DIR.SOUTH: for(i=bbox.y0;i<=bbox.y1 && !nodes;i++) nodes=lookup(pos.x,i); break; case Aios.DIR.WEST: for(i=bbox.x1;i>=bbox.x0 && !nodes;i--) nodes=lookup(i,pos.y); break; case Aios.DIR.EAST: for(i=bbox.x0;i<=bbox.x1 && !nodes;i++) nodes=lookup(i,pos.y); break; default: // radius or full bbox search? for(r=1;r<=bbox.distance;r++) { // TODO } break; } if (nodes) nodes = { distance:distance(pos,nodes[0].position), objects:nodes.map(function (node) { var a = node.getAgent(0); return a?{agent:a.id,class:a.ac,pos:node.position}:undefined }) } } if (name.indexOf('agent')<0) { if (name.indexOf('resource')==0) name=null; // 2. Resources bbox=pp2bbox(bbox) self.rtree.search(bbox).forEach(function (rid) { var obj = self.gui.objects[rid]; var px = pos.x * self.options.patch.width, py = pos.y * self.options.patch.height; if (name && obj.class != name) return; // only resources with distance < nodes.distance are considered var d = distance2Rect({x:px,y:py}, obj.visual, {x:self.options.patch.width, y:self.options.patch.height}) rid=rid.replace(/resource\[([^\]]+)\]/,'$1'); if (nodes && nodes.distance==d) nodes.objects.push({ resource:rid, class:obj.class, data:obj.data, distance:d, x:int(obj.visual.x/self.options.patch.width), y:int(obj.visual.x/self.options.patch.height), w:int(obj.visual.w/self.options.patch.width), h:int(obj.visual.h/self.options.patch.height) }); else if (!nodes || d < nodes.distance) nodes = { distance:d, objects:[{ resource:rid, class:obj.class, data:obj.data, distance:d, x:int(obj.visual.x/self.options.patch.width), y:int(obj.visual.x/self.options.patch.height), w:int(obj.visual.w/self.options.patch.width), h:int(obj.visual.h/self.options.patch.height) }] } }) } return nodes; } else if (typeof who == 'string') { // specific object id // 1. Agent? if (self.cache.agent2node[who]) { return distance(pos,self.cache.agent2node[who].position) } } break; default: remote=false; break; } if (set && callback) { if (Comp.obj.isMatrix(set)) for(p in set) { for (q in set[p]) { if (set2.length) callback.call(current.process.agent,set[p][q],set2[p][q],current.process.agent); else callback.call(current.process.agent,set[p][q],q,p,current.process.agent); } } else if (Comp.obj.isArray(set)) for(p in set) { if (remote && set3.length) { pro=set3[p]; Aios.CB(pro,callback,[pro.agent]) } else callback.call(current.process.agent,set[p],set2[p],current.process.agent); } else { if (remote && set3) { pro=set3; Aios.CB(pro,callback,[pro.agent]) } else callback.call(current.process.agent,set,set2,current.process.agent); } } if (!multiple && set) return set.length==0?null:(set.length==1?set[0]:set); else return set; } /* ** Generic create operation (NetLogo comp., **physical** agents) ** Creates node+agent pair. ** ** Note: callback is executed in that agent context! */ function create(what,num,callback) { var type = whatType(what), node = current.node,nodeId, desc,id,pro, set=[], name=whatName(what); if (!self.options.patch) return; for(var i=0;i tuple; create agent on this node nodeId = aiosXsimu.createNode( name, node.position.x, // use current position node.position.y, name+'-'+instances); if (!nodeId) self.err('create: node class '+name+' cannot be created') id=aiosXsimu.createOn(nodeId,name,{},3); if (self.options.patch) self.agentMap[node.position.y][node.position.x][id]=nodeId; pro=self.getProcess(nodeId,id); // the callback must be executed before // agent starts execution! Prevent transition after CB pro.notransition=true; self.cache.agent2node[id]=pro.node; pro.type=pro.node.type='physical'; // cover arrow functions too, no this rebind possible! first argument is self, too! if (callback) Aios.CB(pro,callback,[pro.agent,i]); set.push(id); instances++; } else { // Create a new computational agent on this node nodeId=node.id; id=aiosXsimu.createOn(nodeId,name,{},3); pro=self.getProcess(nodeId,id); pro.notransition=true; if (callback) Aios.CB(pro,callback,[pro.agent,i]) } break; } return set.length==1? set[0]:set } function die (who) { var node=current.node, agent=current.process.agent; if (!who) { delete self.cache.agent2node[agent.id]; Aios.kill(); if (self.world.getNode(node.id)) aiosXsimu.deleteNode(node.id); } else { // TODO } } function forward(delta) { var i,x,y, node=current.node, _node,_agent, agent=current.process.agent, desc=self.model.agents[agent.ac],id, agents=[current.process], Delta=[0,0]; if (!self.options.patch || desc.type != 'physical') return; id='node['+node.id+']'; var visual=aiosXsimu.getVisual(id); if (!visual.heading) visual.heading=0; if (visual.heading<90) Delta[1] -= delta; else if (visual.heading<180) Delta[0] += delta; else if (visual.heading<270) Delta[1] += delta; else if (visual.heading<360) Delta[0] -= delta; if (node.children) { node.children.forEach(function (child) { agents.push(child.getAgentProcess(0)); }) } // check spatial bounds of all agents to be moved for(i in agents) { _agent=agents[i]; _node=_agent.node; x=_node.position.x+Delta[0]; y=_node.position.y+Delta[1]; if (!self.checkBounds(x,y)) return; } // passed - now move all agents for(i in agents) { _agent=agents[i]; _node=_agent.node; x=_node.position.x+Delta[0]; y=_node.position.y+Delta[1]; aiosXnet.setxy(x,y,'agent',_agent) } } function globals() { return self.model.parameter } function get(p) { var agent=current.process.agent, node=current.node,id, desc=self.model.agents[agent.ac], visual; if (!self.options.patch) return; switch (p) { case 'color': id='node['+node.id+']'; visual=aiosXsimu.getVisual(id); return visual.fill.color; break; case 'heading': id='node['+node.id+']'; visual=aiosXsimu.getVisual(id); return visual.heading; break; case 'shape': id='node['+node.id+']'; visual=aiosXsimu.getVisual(id); return visual.shape; break; } } group = { add: function (parent,children,align) { var agent,desc,node,pos agent=self.world.getAgentProcess(parent); // if (!self.options.patch) return; if (agent) { node = agent.node; pos = Comp.obj.copy(node.position); switch (align) { case Aios.DIR.NORTH: pos.y--; break; case Aios.DIR.SOUTH: pos.y++; break; case Aios.DIR.WEST : pos.x--; break; case Aios.DIR.EAST : pos.x++; break; } if (!self.checkBounds(pos.x,pos.y)) return; desc=self.model.agents[agent.agent.ac]; if (desc.type != 'physical') return; if (!node.children) node.children=[]; children.forEach(function (child) { var agent2 = self.world.getAgentProcess(child); if (agent2) { var desc2=self.model.agents[agent2.agent.ac]; var node2 = agent2.node; if (desc2.type != 'physical') return; var pos2 = Comp.obj.copy(pos); node.children.push(node2); node2.parent = node; // move node container to group position delete self.agentMap[node2.position.y][node2.position.x][agent2.agent.id]; node2.position = pos2; self.agentMap[pos2.y][pos2.x][agent2.agent.id]=node2.id; self.moveObjectTo('node['+node2.id+']', self.world2draw(pos2).x,self.world2draw(pos2).y); } }) } }, rem: function (parent, children) { var agent,desc,node,pos agent=self.world.getAgentProcess(parent); if (agent) { node = agent.node; if (!node.children) return; children.forEach(function (child) { var agent2 = self.world.getAgentProcess(child); if (agent2) { var desc2=self.model.agents[agent2.agent.ac]; var node2 = agent2.node; if (desc2.type != 'physical') return; var pos2 = Comp.obj.copy(pos); node.children=node.children.filter(function (_node) { return _node.id!=node2.id }); node2.parent = null; } }) } }, } // TODO send net agents a signal function signal (destination,signal,argument) { // destination is agent id; // we need the node id, too var agent = self.world.getAgentProcess(destination); return agent?agent.node:'??' } function set(p,v) { var node=current.node, agent=current.process.agent, desc=self.model.agents[agent.ac], obj; if (!self.options.patch) return; switch (p) { case 'color': if (desc.type == 'physical') { // Change color of node and agent id='node['+node.id+']'; aiosXsimu.changeVisual(id,{fill:{color:v}}); id='agent['+agent.ac+':'+agent.id+':'+node.id+']'; aiosXsimu.changeVisual(id,{fill:{color:v}}); } else { // Change color of agent } break; case 'shape': if (desc.type == 'physical') { // Change shape of node id='node['+node.id+']'; aiosXsimu.changeVisual(id,{shape:v,align:v=='circle'?'center':undefined}); } else { // Change color of agent } break; } } // TODO resources, .. function setxy(x,y,what,who) { var type=what||'agent', desc, pos, node=current.node, agent=current.process.agent; // only discrete coordinates allowed (due to pos-map tables) x=x|0; y=y|0; if (!self.options.patch) return; // self.log([x,y,type,agent.ac]) switch (type) { case 'agent': case 'agents': if (typeof who == 'string') { agent=aiosXnet.ask('agent',who); if (!agent) return; } else if (typeof who == 'object') { agent=who.agent; // agent process! node=who.node; } desc=self.model.agents[agent.ac]; if (!desc) return; if (desc.type == 'physical') { // move agent and its node! if (!self.checkBounds(x,y)) return; pos={x:x,y:y}; // checkBounds(x,y) if (node.position) { // Invalidate old worldmap entry delete self.agentMap[node.position.y][node.position.x][agent.id]; } self.agentMap[y][x][agent.id]=node.id; self.moveObjectTo('node['+node.id+']', self.world2draw(pos).x,self.world2draw(pos).y); node.position=pos; // Move child nodes, too (but not parent vice versa)! if (node.children) { node.children.forEach(function (node2) { node2.processes.table.forEach(function (agent2) { if (!agent2) return; if (self.model.agents[agent2.agent.ac] && self.model.agents[agent2.agent.ac].type != 'physical') return; if (node2.position) { // Invalidate old worldmap entry delete self.agentMap[node2.position.y][node2.position.x][agent2.agent.id]; } self.agentMap[y][x][agent2.agent.id]=node2.id; }) self.moveObjectTo('node['+node2.id+']', self.world2draw(pos).x,self.world2draw(pos).y); node2.position=pos; }) } } else { // not supported! } break; } } // Turn agent or group of agents ... function turn(angle) { var node=current.node,_node,_pos,visual,agents, agent=current.process.agent, relative, desc=self.model.agents[agent.ac],id, pos=node.position,delta=0,Delta=[0,0]; switch (angle) { case Aios.DIR.NORTH: angle=0; break; case Aios.DIR.SOUTH: angle=180; break; case Aios.DIR.WEST: angle=270; break; case Aios.DIR.EAST: angle=90; break; default: if (typeof angle == 'number') relative=true; } if (!self.options.patch || desc.type != 'physical') return; id='node['+node.id+']'; visual = aiosXsimu.getVisual(id) if (!visual.heading) visual.heading=0; if (relative) angle=(visual.heading+angle) % 360; delta=angle-visual.heading; // translate group children? if (node.children && node.children.length) { _pos=node.children[0].position; // All children are overlayed !? Delta=[_pos.x-pos.x,_pos.y-pos.y] self.log({from:Delta, to:rotate(Delta,delta)}) Delta=rotate(Delta,delta); node.children.forEach(function (child) { var x=pos.x+Delta[0],y=pos.y+Delta[1]; if (!self.checkBounds(x,y)) return; var _agent = child.getAgentProcess(0); aiosXnet.setxy(x,y,'agent',_agent) }); } // update visual heading parameter visual.heading = angle; } function within(x,y,bbox) { return x >= bbox.x && y >= bbox.y && x < (bbox.x+bbox.w) && y < (bbox.y+bbox.h) } return { ask:ask, create:create, die:die, forward:forward, globals:globals, get:get, group:group, set:set, setxy:setxy, signal:signal, turn:turn, within:within } } module.exports = aiosXnet