From cb7cbb29fa2c2629f95c5ab6f7ec394fedfb7689 Mon Sep 17 00:00:00 2001 From: sbosse Date: Mon, 21 Jul 2025 23:07:00 +0200 Subject: [PATCH] Mon 21 Jul 22:43:21 CEST 2025 --- js/simu/simuWEB.js | 2684 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2684 insertions(+) create mode 100644 js/simu/simuWEB.js diff --git a/js/simu/simuWEB.js b/js/simu/simuWEB.js new file mode 100644 index 0000000..28a1004 --- /dev/null +++ b/js/simu/simuWEB.js @@ -0,0 +1,2684 @@ +/** + ** ============================== + ** O O O OOOO + ** O O O O O O + ** O O O O O O + ** OOOO OOOO O OOO OOOO + ** O O O O O O O + ** O O O O O O O + ** OOOO OOOO O O OOOO + ** ============================== + ** Dr. Stefan Bosse http://www.bsslab.de + ** + ** COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED + ** BY THE AUTHOR(S). + ** THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED, + ** MODIFIED, OR OTHERWISE USED IN A CONTEXT + ** OUTSIDE OF THE SOFTWARE SYSTEM. + ** + ** $AUTHORS: Stefan Bosse + ** $INITIAL: (C) 2006-2024 bLAB + ** $CREATED: 01-02-17 by sbosse. + ** $VERSION: 1.18.21 + ** + ** $INFO: + ** + ** SEJAM: JAM Agent Simluator + ** + WEB/nw.js webix+ Anychart graphics.js GUI + ** + Chat Dialog API (botui) + ** + ** $ENDOFINFO + */ + + +var Comp = Require('com/compat'); +var Io = Require('com/io'); +var current=none; +var Aios = none; + +var Papa = Require('parser/papaparse.js'); +var Esprima = Require('parser/esprima'); +var Name = Require('com/pwgen'); +var Json = Require('jam/jsonfn'); +var util = Require('util'); +var JamAnal = Require('jam/analyzer'); +var Db = Require('db/dbS'); +var DbQ = Require('db/dbQ'); +var SQLJSON = Require('db/sqljson'); +var RTree = Require('rtree/rtree'); +var AiosXnet = Require('simu/aiosXnet'); + +var ml = Require('ml/ml') +var nn = Require('nn/nn') +var csp = Require('csp/csp') +var sat = Require('logic/sat') +var numerics = Require('numerics/numerics'); + +var nameopts = { + world:{length:8, memorable:true, uppercase:true}, + node: {length:8, memorable:true, lowercase:true} +} + +// 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)) +} + +var hrtime; + +if (global.TARGET != "browser") hrtime = process.hrtime; +else { + // polyfil for window.performance.now + var performance = global.performance || {} + var performanceNow = + performance.now || + performance.mozNow || + performance.msNow || + performance.oNow || + performance.webkitNow || + function(){ return (new Date()).getTime() } + + // generate timestamp or delta + // see http://nodejs.org/api/process.html#process_process_hrtime + hrtime = function (previousTimestamp){ + var clocktime = performanceNow.call(performance)*1e-3 + var seconds = Math.floor(clocktime) + var nanoseconds = Math.floor((clocktime%1)*1e9) + if (previousTimestamp) { + seconds = seconds - previousTimestamp[0] + nanoseconds = nanoseconds - previousTimestamp[1] + if (nanoseconds<0) { + seconds-- + nanoseconds += 1e9 + } + } + return [seconds,nanoseconds] + } +} + +/* 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} +} +/** Create a simulation world with visualization + * + * options: {config:object,id:string?, + * nolimits:boolean is a disable flag for agent check poininting, + * connection:{random:number?},markings?, + * fastcopy?, + * gui, + * log:function, + * msg:function, + * units? : {}, + * format? : {node,agent,class,...} is logging format setting, + * classes:{node:{..},ac1:{..},..}} + * with config: + * + */ +var simu = function (options) { + var self=this; + var node1,node2,row,row1,row2,i,j,p; + + this.options=options||{}; + + if (this.options.id==_) this.options.id=Name.generate(nameopts.world); + if (this.options.connections==_) this.options.connections={}; + if (this.options.markings) this.options.nodebar=true; + if (this.options.nolimits) Aios.config({nolimits:true}); + + this.classes=this.options.classes||{}; + delete this.options.classes; + this.run=false; // Simulator running + this.stopped=false; // Forced stop + this.step=0; // Step counter + this.stepped=0; // Remaining steps before stopping + this.loop=none; + this.time=0; // Current simulation time + this.time0=0; + this.lag=0; // Time lag between simulation runs + this.events=[]; + this.parameter=options.parameter||{}; + this.verbose = options.verbose||0; + Aios.config({verbose:this.verbose}); + + this.cleanWorld=false; + + this.log=options.log; // Simulation messaging + this.msg=options.msg; // Agent messaging + Aios.config({print:options.log}); + // Aios.World.options.verbose=1; + + this.UI = options.gui.UI; + this.webix = options.gui.webix; + this.gui = options.gui; + this.simuPhy = options.simuPhy; + this.CANNON = options.CANNON; + + this.stats = { + agents:{}, + custom:{}, + cpu:0 + }; + + // Caches + this.cache = { + agent2node:{} + } + + // Region tree for resources + this.rtree = RTree(10); + + this.err=function (msg,err) { + self.log('Error: '+msg); + throw (err||'[SIM] Error'); + } + + this.warn=function (msg) { + self.log('Warning: '+msg); + } + + this.out=function (msg) { + self.log(msg); + } + + + if (options.time!='real') + Aios.config({time:function () { + return self.step; + }}),this.log('Setting simulation clock to step time!') ; + if (!options.format) this.options.format={}; // Agent logging format + this.options.format.forEach(function (v,a) { Aios.config(v?{'log+':a}:{'log-':a})}); + this.out('[SIM] Logging flags: '+util.inspect(this.options.format)); + + Aios.off('agent+'); + Aios.on('agent+',function (desc) { + var aid,nid='node['+desc.node.id+']', + visual,xn,yn,level=desc.node.position.z||0; + if (desc.agent.ac && self.world.classes[desc.agent.ac]) { + if (!self.stats.agents[desc.agent.ac]) self.stats.agents[desc.agent.ac]=0; + self.stats.agents[desc.agent.ac]++; + aid='agent['+desc.agent.ac+':'+desc.agent.id+':'+desc.node.id+']'; + visual=self.world.classes[desc.agent.ac].visual; + //self.out('Event register '+desc.agent.id+' of class '+desc.agent.ac+' on node '+desc.node.id+ + // '('+self.gui.objects[nid].visual.x+','+self.gui.objects[nid].visual.y+')'); + if (visual) { + xn=self.gui.objects[nid].visual.x+self.gui.objects[nid].visual.w/2; + yn=self.gui.objects[nid].visual.y+self.gui.objects[nid].visual.h/2; + if (visual.x!=undefined && visual.y!=undefined) { + xn=xn+visual.x; + yn=yn+visual.y; + } + if (visual.center && visual.center.x!=undefined && visual.center.y!=undefined) { + xn=xn+visual.center.x; + yn=yn+visual.center.y; + } + self.gui.objects[aid] = { + visual:{ + x:xn, + y:yn, + shape:visual.shape||'circle', + w:visual.width, + h:visual.height, + fill:visual.fill, + line:visual.line, + level:level + }, + obj:desc.agent, + id:aid + }; + self.gui.drawObject(aid); + } + } else { + } + }); + + Aios.off('agent-'); + Aios.on('agent-',function (desc) { + var aid='agent['+desc.agent.ac+':'+desc.agent.id+':'+desc.node.id+']'; + //self.out('Event unregister '+proc.agent.id+' on node '+node.id); + self.gui.destroyObject(aid); + if (self.cache.agent2node[desc.agent.id]) delete self.cache.agent2node[desc.agent.id]; + if (desc.proc.type=='physical' && desc.node.type=='physical' && self.world.getNode(desc.node.id)) { + self.deleteNode(desc.node.id); + } + }); + + Aios.off('signal+'); + Aios.on('signal+',function (proc,node,sig,arg,from) { + var aid='agent['+proc.agent.ac+':'+proc.agent.id+':'+node.id+']', + sid='signal['+proc.agent.ac+':'+proc.agent.id+':'+node.id+']', + nid='node['+node.id+']', + visual,level=node.position.z||0; + // self.log(aid); + if (self.model.classes[proc.agent.ac] && self.model.classes[proc.agent.ac].on) + visual=self.model.classes[proc.agent.ac].on[sig]; + if (visual) { + xn=self.gui.objects[nid].visual.x+self.gui.objects[nid].visual.w/2; + yn=self.gui.objects[nid].visual.y+self.gui.objects[nid].visual.h/2; + if (visual.x && visual.y) { + xn=xn+visual.x; + yn=yn+visual.y; + } + self.gui.objects[sid] = { + visual:{ + x:xn, + y:yn, + shape:visual.shape||'circle', + w:visual.width, + h:visual.height, + fill:visual.fill, + line:visual.line, + level:level, + timeout:visual.time + }, + obj:{sig:sig,arg:arg,from:from}, + id:sid + }; + self.gui.drawObject(sid); + self.cleanWorld=true; + } + + }); + + // PRINT: Smart print function + var print = function (msg,header,depth,nolog) { + var lines=[]; + var line=''; + if (depth==_) depth=1; + if (msg==undefined) msg='undefined'; + if (msg==null) msg='null'; + function isvec(obj) {return(Comp.obj.isArray(obj) && (obj.length == 0 || !Comp.obj.isArray(obj[0])))} + function ismat(obj) {return(Comp.obj.isArray(obj) && obj.length > 0 && Comp.obj.isArray(obj[0]))} + function mat(o,depth) { + // matrix + var lines=[]; + var line = ''; + if (header) {line=header; header=_}; + for (var j in o) { + var row=o[j]; + line += Comp.printf.list(row,function (v) { + return (Comp.obj.isArray(v)?(depth>0?'['+vec(v,depth-1)+']':'[..]'): + Comp.obj.isObj(v)?(depth>0?obj(v,depth-1):'{..}'):v); + }); + lines.push(line); + line=''; + } + return lines; + } + function vec(v,depth) { + // vector + var lines=[]; + var line = ''; + if (header) {line=header; header=_}; + if (v.length==0) return(line+'[]'); + else { + // can still contain matrix elements that must bes separated + var sep='',sepi=''; + for (var p in v) { + if (ismat(v[p])) { + //self.log.log(line); line=' '; + if (depth>0) { + lines = mat(v[p],depth-1); + line += sep+'['; sepi=''; + Comp.array.iter(lines,function (line2) { + line += sepi+'['+line2+']'; + sepi=','; + }); + line += ']'; + sep=','; + } else { + line += sep+'[[..]]'; + sep=','; + } + } + else if (isvec(v[p])) { + //self.log.log(line); line=' '; + line += sep+vec(v[p],depth-1); + sep=','; + } + else { + line += sep+(Comp.obj.isArray(v[p])?(depth>0?vec(v[p],depth-1):'[..]'): + Comp.obj.isObj(v[p])?(depth>0?obj(v[p],depth-1):'{..}'):v[p]); + sep=','; + } + } + if (line!='') return line; + } + } + function obj(o,depth) { + var line=''; + var sep=''; + if (header) {line=header; header=_}; + line += '{'; + for (var p in o) { + if (!Comp.obj.isFunction(o[p])) { + line += sep + p+':'+ + (Comp.obj.isArray(o[p])?(depth>0?vec(o[p],depth-1):'[..]'): + Comp.obj.isObj(o[p])?(depth>0?obj(o[p],depth-1):'{..}'):o[p]); + sep=','; + } else { + line += sep + p+':'+'function()'; + sep=','; + } + } + return line+'}'; + } + + function str(s) { + var line=''; + var lines=[]; + var lines2 = Comp.string.split('\n',msg); + if (header) {line=header; header=_}; + if (lines2.length==1) + lines.push(line+msg); + else { + Comp.array.iter(lines2,function (line2,i) { + if (i==0) lines.push(line+line2); + else lines.push(line2); + }); + } + return lines; + } + + if (Comp.obj.isError(msg)) { + if (header) {line=header; header=_}; + line += (/Error/.test(msg.toString())?msg.toString():'Error: '+msg.toString()); + lines.push(line); + } else if (ismat(msg)) lines = Comp.array.concat(lines, + Comp.array.map(mat(msg,depth-1),function (line){ + return ' '+line})); + else if (Comp.obj.isString(msg)) lines = Comp.array.concat(lines,str(msg)); + else if (isvec(msg)) lines.push(vec(msg,depth-1)); + else if (Comp.obj.isObj(msg)) lines.push(obj(msg,depth-1)); + else { + if (header) {line=header; header=_}; + line += msg; + lines.push(line); + } + + if (nolog) return lines; else Comp.array.iter(lines,function (line) {self.msg(line)}); + }; + + var msg = function(){ + var header=''; + if (!current.node || !current.process) { + print('[SIM] '+arguments[0]); + } + else if (arguments.length==1) { + header='['; + if (self.options.format.time) header += (Aios.time()+':'); + if (self.options.format.node) header += (current.node.id+':'); + header += (current.process.agent.id+':'); + if (self.options.format.pid) header += (current.process.pid+':'); + if (self.options.format.class) header += (current.process.agent.ac+''); + header += '] '; + print(arguments[0],header); + } else { + header='['; + if (self.options.format.time) header += (Aios.time()+':'); + if (self.options.format.node) header += (current.node.id+':'); + header += (current.process.agent.id+':'); + if (self.options.format.pid) header += (current.process.pid+':'); + if (self.options.format.class) header += (current.process.agent.ac+''); + header += '] '; + for (var i in arguments) { + if (i==0) + print(arguments[i],'['+current.node.id+':'+current.process.agent.id+':'+current.process.pid+':'+current.process.agent.ac+'] '); + else + print(arguments[i],_,2); + } + } + }; + + // Module extensions + Aios.ml = ml.agent; + Aios.aios1.ml = ml.agent; + Aios.aios2.ml = ml.agent; + Aios.aios3.ml = ml.agent; + Aios.nn = nn.agent; + Aios.aios1.nn = nn.agent; + Aios.aios2.nn = nn.agent; + Aios.aios3.nn = nn.agent; + Aios.sat = sat.agent; + Aios.aios1.sat = sat.agent; + Aios.aios2.sat = sat.agent; + Aios.aios3.sat = sat.agent; + Aios.csp = csp.agent; + Aios.aios1.csp = csp.agent; + Aios.aios2.csp = csp.agent; + Aios.aios3.csp = csp.agent; + Aios.numerics = numerics.agent; + Aios.aios1.numerics = numerics.agent; + Aios.aios2.numerics = numerics.agent; + Aios.aios3.numerics = numerics.agent; + + // Extended sandbox environment available for all agents + Aios.aios0.log=msg; + Aios.aios1.log=msg; + Aios.aios2.log=msg; + Aios.aios3.log=msg; + + Aios.err=function(_msg) {msg('[AIOS] Error: '+_msg)}; + Aios.aios0.print=function(msg,depth) {return print(msg,_,depth,false)}; + Aios.aios1.print=function(msg,depth) {return print(msg,_,depth,false)}; + Aios.aios2.print=function(msg,depth) {return print(msg,_,depth,false)}; + Aios.aios3.print=function(msg,depth) {return print(msg,_,depth,false)}; + Aios.aios0.sprint=function(msg,depth) {return print(msg,_,depth,true)}; + Aios.aios1.sprint=function(msg,depth) {return print(msg,_,depth,true)}; + Aios.aios2.sprint=function(msg,depth) {return print(msg,_,depth,true)}; + Aios.aios3.sprint=function(msg,depth) {return print(msg,_,depth,true)}; + //Aios.aios0.inspect=function(msg,depth) {return util.inspect(msg)}; + //Aios.aios1.inspect=function(msg,depth) {return util.inspect(msg)}; + //Aios.aios2.inspect=function(msg,depth) {return util.inspect(msg)}; + //Aios.aios3.inspect=function(msg,depth) {return util.inspect(msg)}; + Aios.aios3.sensors=function () { + return Aios.aios0.copy(current.node.sensors); + }; + Aios.aios0.Vec3=function(x,y,z) {return new self.CANNON.Vec3(x,y,z)}; + Aios.aios1.Vec3=function(x,y,z) {return new self.CANNON.Vec3(x,y,z)}; + Aios.aios2.Vec3=function(x,y,z) {return new self.CANNON.Vec3(x,y,z)}; + Aios.aios3.Vec3=function(x,y,z) {return new self.CANNON.Vec3(x,y,z)}; + + Aios.config({iterations:1,fastcopy:options.fastcopy||false}); + + if (!this.options.nolimits) Aios.config({TIMESCHED:1000}); + + // Create the simulation world + this.world = Aios.World.World([],{id:this.options.id,classes:this.classes}); + + /* optional geographical (gps) or geometric (geo) mapping functions ({x,y,z}) -> {latitude,longitude,height} + ** { gps2px, px2gps, geo2px, px2geo, gps2dist } + ** Can be used for absolute or relative coordinate transformations ({delta:true}). + */ + this.world.units = options.units||{gps2dist:function () {}}; + + // old channel-based SQL DB connection + this.db = { + init: function (path,channel,callback) { + var proc=current.process,stat; + if (!self.Db) self.Db={}; + if (!self.Db[path]) { + if (channel==undefined) { + // embedded SQL + self.Db[path]=DbQ.Sql(path); + stat=self.Db[path].open(); + if (callback) proc.callback(callback,[stat]); + } else { + // remote SQL + self.Db[path]=Db.Sqlc(path,channel); + self.Db[path].setLog(function (msg) {self.log(msg)}); + self.Db[path].init(function (stat,err) { + self.log('[DB] '+path+' Initialized: '+stat); + if (callback) proc.callback(callback,[stat,err]); + }); + } + } + }, + // function (path:string,matname:string,header:[$var:$type]|[$type], callback?) + createMatrix: function (path,matname,header,callback) { + var proc=current.process; + if (callback) self.Db[path].createMatrix(matname,header,function (stat,err) { + if (callback) proc.callback(callback,[stat,err]); + }); else return self.Db[path].createMatrix(matname,header); + }, + // function (path:string,matname:string,header:{$var:$type}, callback?) + createTable: function (path,tblname,header,callback) { + var proc=current.process; + if (callback) self.Db[path].createTable(tblname,header,function (stat,err) { + if (callback) proc.callback(callback,[stat,err]); + }); else return self.Db[path].createTable(tblname,header); + }, + drop: function (path,tbl,callback) { + var proc=current.process; + self.Db[path].drop(tbl,function (stat,err) { + if (callback) proc.callback(callback,[stat,err]); + }); + }, + error: function (path) { + return self.Db[path].error + }, + exec: function (path,cmd,callback) { + var proc=current.process; + if (callback) self.Db[path].exec(cmd,function (stat,err) { + if (callback) proc.callback(callback,[stat,err]); + }); else return self.Db[path].exec(cmd); + }, + get: function (path,callback) { + var proc=current.process; + self.Db[path].get(undefined,function (row,err) { + if (callback) proc.callback(callback,[row,err]); + }); + }, + insert: function (path,tbl,row,callback) { + var proc=current.process; + self.Db[path].insert(tbl,row,function (stat,err) { + if (callback) proc.callback(callback,[stat,err]); + }); + }, + insertMatrix: function (path,matname,row,callback) { + var proc=current.process; + if (callback) self.Db[path].insertMatrix(matname,row,function (stat,err) { + if (callback) proc.callback(callback,[stat,err]); + }); else return self.Db[path].insertMatrix(matname,row); + }, + insertTable: function (path,tblname,row,callback) { + var proc=current.process; + if (callback) self.Db[path].insertTable(tblname,row,function (stat,err) { + if (callback) proc.callback(callback,[stat,err]); + }); else return self.Db[path].insertTable(tblname,row); + }, + readMatrix: function (path,matname,callback) { + var proc=current.process; + if (callback) self.Db[path].readMatrix(matname,function (mat,err) { + if (callback) proc.callback(callback,[mat,err]); + }); else return self.Db[path].readMatrix(matname); + }, + readTable: function (path,tblname,callback) { + var proc=current.process; + if (callback) self.Db[path].readTable(tblname,function (tbl,err) { + if (callback) proc.callback(callback,[tbl,err]); + }); else return self.Db[path].readTable(tblname); + }, + select: function (path,tbl,vars,cond,callback) { + var proc=current.process; + if (!callback && Comp.obj.isFunction(cond)) callback=cond,cond=undefined; + self.Db[path].select(tbl,vars,cond,function (stat,err) { + if (callback) proc.callback(callback,[stat,err]); + }); + }, + writeMatrix: function (path,matname,matrix,callback) { + var proc=current.process; + if (callback) self.Db[path].writeMatrix(matname,matrix,function (stat,err) { + if (callback) proc.callback(callback,[stat,err]); + }); else return self.Db[path].writeMatrix(matname,matrix); + }, + writeTable: function (path,tblname,table,callback) { + var proc=current.process; + if (callback) self.Db[path].writeTable(tblname,table,function (stat,err) { + if (callback) proc.callback(callback,[stat,err]); + }); else return self.Db[path].writeTable(tblname,table); + }, + } + // New SQLJSOn RPC API + this.sql = { + init : function (url) { + return SQLJSON.sql(url) + }, + } + + // CSV file import/export + this.csv = { + read: function (file,callback,verbose) { + var text,data,header,convert,proc=current.process; + if (callback == true) convert=true,callback=null; + if (verbose) self.log('CSV: Reading from '+file); + try { + text=Io.read_file(file); + if (!text) throw 'CSV File read error: '+file; + if (verbose) self.log('CSV: Parsing '+file); + Papa.parse(text,{ + skipEmptyLines: true, + dynamicTyping: true, + complete: function(results) { + if (verbose) self.log('CSV parsed with DEL="'+results.meta.delimiter+'" TRUNC='+results.meta.truncated+ + ' ABORT='+results.meta.aborted); + if (callback) proc.callback(callback,[results.data]); + data=results.data; + } + }); + if (convert) { // first line must be header + header=data.shift(); + data=data.map(function (row) { + var r={}; + header.forEach(function (col,i) { r[col]=row[i] }); + return r; + }) + } + return data; + } catch (e) { + if (callback) callback(e); + return e; + } + }, + write: function (file,header,data,callback,verbose) { + if (file[0]!='/') file=self.gui.workdir+'/'+file; + var d1=false,fd,i,convert=!Comp.obj.isArray(data[0])&&Comp.obj.isObj(data[0]), + sep=',',proc=current.process; + if (typeof callback == 'string') sep=callback,callback=null; + d1 = typeof data[0] != 'object'; + if (!header || header.length==0) { + if (!convert) + header=d1?['0']:data[0].map(function (x,i) { return String(i) }); + else { + header=[]; + for (var p in data[0]) { + header.push(p); + } + } + } + try { + if (verbose) self.log('CSV: Writing to '+file); + fd=Io.open(file,'w+'); + Io.write_line(fd,header.join(sep)); + if (!d1) + for(i in data) { + if (!convert) + Io.write_line(fd,data[i].join(sep)); + else + Io.write_line(fd,header.map(function (col) { return data[i][col]}).join(sep)); + } + else + for(i in data) { + if (!convert) + Io.write_line(fd,data[i]); + else + Io.write_line(fd,data[i][header[0]]); + }; + + Io.close(fd); + if (callback) proc.callback(callback,[data.length]); + return data.length + } catch (e) { + if (callback) proc.callback(callback,[e]); + return e; + } + } + }; + + + + /* utils */ + function within(p1,p2,r) { + return (p1.x>= p2.x-r && p1.x <= p2.x+2) && + (p1.y>= p2.y-r && p1.y <= p2.y+2) + } + 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 simulator interface + var aiosXsimu = { + changeVisual: function (id,visual) { + self.gui.changeObject(id,visual); + }, + + chat : { + agent : self.gui.chatAgent, + message: self.gui.chatMessage, + question : function (id, question, action, timeout, callback) { + var process = Aios.current.process; + self.gui.chatQuestion (id, question, action, function (res) { + process.callback(callback,res?[res.value]:null); + process.wakeup(); + },timeout); + process.suspend(Aios.timeout(timeout)); + }, + script: self.gui.chatScript, + }, + + clear: function (msg,log) { + if (msg) { + self.gui.clear('msgWin'); + self.gui.messages=[]; + } + if (log) self.gui.clear('logWin'); + }, + + /* + ** typeof arguments = {nodeclass:string|function,arg1,arg2,...} + ** arguments arg1,.. are node constructor function arguments + ** if nodeclass is a string then a function of model.nodes.(arg1,arg2,..) or + ** model.world.nodes is used + ** returns node identifier + */ + createNode: function () { + var nodes = (self.model.world && self.model.world.nodes) || + self.model.nodes,res, + cls=typeof arguments[0] == 'string'?arguments[0]:'function'; + function shift(args) { + var i=0,p; + for(p in args) { + if (p=='0') continue; + args[i]=args[p]; + i++; + } + return args; + } + if (nodes) { + var constrFun; + if (Comp.obj.isFunction(arguments[0])) constrFun=arguments[0]; + else constrFun=nodes[arguments[0]]; + if (constrFun && Comp.obj.isFunction(constrFun)) { + var desc=constrFun.apply(null,shift(arguments)); + if (desc) { + if (res=self.checkVisual(desc.visual)) throw 'Invalid node ('+cls+') visual: '+res; + return self.createNode(desc,true); + } + } else self.log('createNode: unknown node class '+arguments[0]) + } else self.log('createNode: no node constructors defined (expected model.world.nodes or model.nodes') + }, + + /* + ** Create an agent on specified node + */ + createOn:function (nid,ac,args,level) { + var node=self.world.getNode(nid), + proc=none; + if (!node) return none; + if (level==undefined) level=1; + if (args==undefined) args=[{}]; + if (self.world.classes[ac]) + agent = Aios.Code.createOn(node,self.world.classes[ac][level],args,level,ac); + else + self.err('createOn: no such class '+ac); + if (agent) + return agent.agent.id; + else return none; + }, + + deleteNode: function (nodeid) { + self.deleteNode(nodeid); + }, + + event: { + add: function (ev) { + self.events.push({step:self.step,node:current.node.id,event:ev}); + }, + get: function () { + var evl = self.events; + self.events=[]; + return evl; + } + }, + + get : function (p) { + switch (p) { + case 'delay': return self.delay; + } + }, + + // Return node object (if id is a regular expression, an array of matching nodes) + getNode:function (id) { + return self.world.getNode(id); + }, + + getVisual: function (id) { + return self.gui.getObject(id).visual; + }, + + getStats: function (target,arg) { + var i,j,proc,node,p, + stats; + switch (target) { + case 'node': + node=self.world.getNode(arg); + if (node) stats=node.stats; + break; + default: + stats={steps:self.step,time:Aios.time(),agents:{},agentsN:self.stats.agents,total:0,nodes:0, + create:0,migrate:0,fork:0,fastcopy:0,signal:0, + send:0, + cpu:self.stats.cpu, + custom:{}} + for(p in self.stats.custom) stats.custom[p]=self.stats.custom[p]; + if (!self.world || !self.world.nodes) return {}; + for (i in self.world.nodes) { + node=self.world.nodes[i]; + if (!node) continue; + stats.nodes++; + for (p in node.stats) stats[p] += node.stats[p]; + for(j in node.processes.table) { + proc=node.processes.table[j]; + if (proc && proc.agent.ac) { + if (stats.agents[proc.agent.ac]==undefined) stats.agents[proc.agent.ac]=0; + stats.agents[proc.agent.ac]++; + stats.total++; + } + } + for(p in node.connections) { + if (node.connections[p] && node.connections[p].count) + stats.send += node.connections[p].count(); + } + } + break; + } + return stats; + }, + + getSteps:function () {return self.step}, + + // Load JSON object from file + load : function (file) { + var json; + if (global.config.os=='win32' && global.config.win) { + json = Io.read_file('Z:'+self.gui.lastdir+'\\'+file); + self.gui.log('Z:'+self.gui.lastdir+'\\'+file) + } else if (global.config.os=='win32') { + json = Io.read_file(self.gui.lastdir+'\\'+file); + self.gui.log(self.gui.lastdir+'\\'+file) + } else if (global.TARGET != 'browser') { + json = Io.read_file(self.gui.lastdir+'/'+file); + self.gui.log(self.gui.lastdir+'/'+file) + } else { + json = loadFile(file); + self.gui.log(file) + } + if (json) return aiosXsimu.ofJSON(json); + }, + + log: function (msg) { + self.gui.log(msg); + }, + + inspect : util.inspect, + + model : function (name) { + return self.model; + }, + message:this.gui.message, + // relative move dx,dy,dz with|w/o units m | \B0 .. + + move: self.moveObject.bind(this), + // absolute move to x,y,z with|w/o units m | \B0 .. + + moveTo: self.moveObjectTo.bind(this), + + network: { + x:this.options.x, + y:this.options.y, + z:this.options.z + }, + + ofJSON: function (s) { + return Aios.Code.Jsonf.parse(s,{}) + }, + + options:this.options, + + parameter: function (name) { + return name?self.parameter[name]:self.parameter; + }, + + position: function () { return current.node.position }, + + print:print, + + // Save object to JSON file + save : function (file,o) { + var json = aiosXsimu.toJSON(o),res; + if (global.config.os=='win32' && global.config.win) + res = Io.write_file('Z:'+self.gui.lastdir+'\\'+file,json); + else if (global.config.os=='win32') + res = Io.write_file(self.gui.lastdir+'\\'+file,json); + else + res = Io.write_file(self.gui.lastdir+'/'+file,json); + + return res + }, + + // Set simulation control parameter and GUI control + set : function (p,v) { + var old; + switch (p) { + case 'delay': old=self.delay; self.delay=v; break; + case 'window': + if (Comp.obj.isObj(v)) for(var o in v) { + console.log(o,v[o]) + switch (o) { + case 'layout': + if (v[o]=='auto') { + var _locked=self.gui.toolbar.locked; + self.gui.toolbar.locked=false; + self.gui.autoLayout(); + self.gui.toolbar.locked=_locked; + } + break; + case 'Inspector': + case 'inspector': + if (self.gui.inspectorWin[v[o]]) self.gui.inspectorWin[v[o]](); + break; + case 'Chat': + case 'chat': + if (self.gui.chatWin[v[o]]) self.gui.chatWin[v[o]](); + if (v[o]=='show') self.gui.chatInit(); + break; + case 'World': + case 'world': + if (self.gui.worldWin.container) self.gui.worldWin.container.resume() + if (v[o]['zoom'] && v[o]['zoom']=='fit') self.gui.worldWin.zoomFit(); + break; + } + } + break; + } + return old; + }, + + start: function (steps) { self.start(steps,true)}, + + stat: function (p,v) { + self.stats.custom[p]=v; + }, + + stop: function () { return self.stop(true) }, + // Use simulation time (steps) instead of system time + + toJSON: function (o) { + return Aios.Code.Jsonf.stringify(o) + }, + + time : function () {return self.step}, + + utime: function () { + hr=hrtime(); return hr[0]*1E9+hr[1] + }, + }; + + aiosXsimu.csv = this.csv; + aiosXsimu.db = this.db; + aiosXsimu.sql = this.sql; + aiosXsimu.units = this.world.units; + + // Simulation extension for phyiscal (behaviorual) agents + var aiosXnet = AiosXnet.call(self,aiosXsimu,Aios); + + // if (this.simuPhy) .. + // always set-up API for optional physical simulation + // -> simuPhy will be usually created AFTER simuWeb! + aiosXsimu.simuPhy = aiosXsimu.phy = { + changeScene: function (scene,options) { + if (!self.simuPhy) return; + self.simuPhy.changeScene(scene,options); + }, + get : function (id) { + var tokens, + obj; + if (!self.simuPhy) return; + tokens = (id||Aios.current.node.id).split(','); + obj = self.simuPhy.get(tokens.map(function (s) {return Number(s)})); + return obj; + }, + refresh: function () { + if (!self.simuPhy) return; + self.simuPhy.refresh(); + }, + step: function (n,callback) { + if (!self.simuPhy) return; + return self.simuPhy.step(n,callback); + }, + stepPhyOnly: function (n,callback) { + if (!self.simuPhy) return; + self.simuPhy.stepPhyOnly(n,callback); + } + }; + + // Extend corefuncs in JAM analyzer ... + // MUST BE COMPLETE - else analyzer fails! + JamAnal.extend({ + // keep consistent with aiosXnet.js API + net : { + obj : { + ask:{argn:[2,3]}, + create:{argn:[2,3]}, + die:{argn:[0,1]}, + forward:{ argn:[0,1]}, + get:{argn:1}, + globals:{argn:0}, + group : { obj : { + add: {argn:[2,3] }, + rm: {argn:2 } + }}, + set:{argn:2}, + setxy:{argn:2}, + turn:{ argn:1}, + within:{ argn:2}, + } + }, + simu:{ + obj:{ + chat : { + obj : { + agent:{argn:1}, + message:{argn:2}, + question:{argn:5}, + script:{obj:{ + init:{argn:2}, + next:{argn:0}, + cancel:{argn:0}, + reset:{argn:0} + }} + } + }, + changeVisual:{argn:2}, + clear:{argn:[0,1,2]}, + createNode:{argn:[1,2,3,4,5,6,7,8,9]}, + createOn:{argn:[2,3,4]}, + csv:{ obj:{ + read:{argn:[1,2,3]}, + write:{argn:[2,3,4]}, + }}, + db: { obj :{ + error:{argn:1}, + init:{argn:3}, + createMatrix:{argn:[2,3]}, + createTable:{argn:[2,3]}, + drop:{argn:[2,3]}, + insert:{argn:[3,4]}, + }}, + deleteNode:{argn:1}, + event:{obj:{add:{argn:1},get:{argn:0}}}, + get:{argn:1}, + getNode:{argn:1}, + getStats:{argn:0}, + getSteps:{argn:0}, + inspect:{argn:[1,2,3,4,5,6,7,8,9]}, + load:{argn:1}, + log:{argn:1}, + message:{argn:1}, + model:{argn:0}, + move: {argn:[3,4]}, + moveTo: {argn:[3,4]}, + ofJSON: {argn:1}, + parameter:{argn:0}, + position : {argn:0}, + phy : { + obj : { + changeScene:{argn:2}, + get:{argn:1}, + refresh:{argn:0}, + step:{argn:[1,2]}, + stepPhyOnly:{argn:[1,2]}, + } + }, + save:{argn:2}, + set: {argn:2}, + simuPhy : { + obj : { + changeScene:{argn:2}, + get:{argn:1}, + refresh:{argn:0}, + step:{argn:[1,2]}, + stepPhyOnly:{argn:[1,2]}, + } + }, + sql : { + init : {argn:1}, + }, + start: {argn:1}, + stat: {argn:2}, + stop: {argn:0}, + toJSON: {argn:1}, + time: {argn:0}, + units: {obj:{gps2dist:{argn:2}}}, + utime:{argn:0}, + } + } + }); + + // Extended sandbox environment available for all agents in simulation + this.aiosX = aiosXsimu; + // Aios.aios0.simu=this.aiosX; + // Aios.aios1.simu=this.aiosX; + Aios.aios2.simu=this.aiosX; + Aios.aios3.simu=this.aiosX; + // only immobile level-3 phyiscal agents (on mobile nodes) can use the net API + Aios.aios3.net=aiosXnet; + + if (options.aios) { + // Extend AIOS environment(s) + for (p in options.aios) { + if (!Aios.aios0[p]) Aios.aios0[p]=options.aios[p]; + if (!Aios.aios1[p]) Aios.aios1[p]=options.aios[p]; + if (!Aios.aios2[p]) Aios.aios2[p]=options.aios[p]; + if (!Aios.aios3[p]) Aios.aios3[p]=options.aios[p]; + } + } + if (options.aios0) { + // Extend AIOS environment(s) + for (p in options.aios0) { + if (!Aios.aios0[p]) Aios.aios0[p]=options.aios[p]; + } + } + if (options.aios1) { + // Extend AIOS environment(s) + for (p in options.aios1) { + if (!Aios.aios1[p]) Aios.aios1[p]=options.aios[p]; + } + } + if (options.aios2) { + // Extend AIOS environment(s) + for (p in options.aios2) { + if (!Aios.aios2[p]) Aios.aios2[p]=options.aios[p]; + } + } + if (options.aios3) { + // Extend AIOS environment(s) + for (p in options.aios3) { + if (!Aios.aios3[p]) Aios.aios3[p]=options.aios[p]; + } + } + this.delay=0; + + this.out('[SIM] Simulation created.'); +} + + +simu.prototype.analyze=JamAnal.jamc.prototype.analyze; +simu.prototype.syntax=JamAnal.jamc.prototype.syntax; + +/** Add an agent template class + * + */ +simu.prototype.addClass = function (constructor,name,visual) { + var self=this, + modu, + content, + off, + syntax, + regex1, + env={}, + interface, + p, + text; + + try { + content = 'var ac = '+constructor; + this.out('Parsing agent class template "'+name+'" ...'); + syntax = Esprima.parse(content, { tolerant: true, loc:true }); + if (syntax.errors && syntax.errors.length>0) { + throw syntax.errors[0]; + } + interface=this.analyze(syntax,{classname:name,level:3,verbose:1,err:function (msg) { self.out(msg); throw msg}}); + this.out('Agent class template "'+name+'" analyzed successfully.'); + text=Json.stringify(constructor); + // regex1= /this\.next=([a-zA-Z0-9_]+)/; + // text=text.replace(regex1,"this.next='$1'"); + // console.log(text); + for (p in interface.activities) env[p]=p; + with (env) { eval('constructor='+text) }; + this.out('Agent class template "'+name+'" compiled successfully.'); + this.out('Adding agent class constructor "'+name+'" ...'); + this.world.classes[name]=[ + Aios.Code.makeSandbox(constructor,0,env), + Aios.Code.makeSandbox(constructor,1,env), + Aios.Code.makeSandbox(constructor,2,env), + Aios.Code.makeSandbox(constructor,3,env) + ]; + this.world.classes[name].visual=visual; + } catch (e) { + this.out('Agent class template "'+name+'" not compiled successfully: '+e); + this.gui.message(e); + if (this.verbose>1) this.out(Io.sprintstack(e)); + } + +}; + +/** Check coordinates in boundaries of the current world + * + */ +simu.prototype.checkBounds = function (x,y,z) { + if (x < 0 || y < 0) return false; + if (this.options.x && x >= this.options.x) return false; + if (this.options.y && y >= this.options.y) return false; + return true; +} + +simu.prototype.checkVisual = function (v) { + if (!v) return 'missing visual'; + if (['circle','rect','icon'].indexOf(v.shape)<0) return 'invalid shape'; +} + +/** Convert coordinates from one unit system to another + * + */ +simu.prototype.convertUnit = function (p,from,to) { + if (Comp.obj.isString(p)) { + // Convert to px (delta only) + if (Comp.string.endsWith(p,'m')) + return this.world.units.geo2px({delta:true,X:Comp.string.prefix(p,'m')}).x; + else if (Comp.string.endsWith(p,'\B0')) + return this.world.units.gps2px({delta:true,longitude:Comp.string.prefix(p,'\B0')}).x + else if (Comp.string.endsWith(p,'px')) + return Number(Comp.string.prefix(p,'px')); + else Number(p); + } else switch (from) { + case 'px': + switch (to) { + case 'px': + return p; + case 'm': + if (this.world.units.px2geo) return this.world.units.px2geo(p); + break; + case '\B0': + if (this.world.units.px2gps) return this.world.units.px2gps(p); + break; + } + break; + case 'm': + switch (to) { + case 'px': + if (this.world.units.geo2px) return this.world.units.geo2px(p); + break; + case 'm': + return p; + case '\B0': + if (this.world.units.geo2gps) return this.world.units.geo2gps(p); + break; + } + break; + case '\B0': + switch (to) { + case 'px': + if (this.world.units.gps2px) return this.world.units.gps2px(p); + break; + case 'm': + if (this.world.units.gps2geo) return this.world.units.gps2geo(p); + break; + case '\B0': + return p; + } + break; + } +} + +/** Create a regular (or irregular) 2d mesh grid of nodes and connections. + * type of options = {width: number as node width in pixel, + height: number as node heigth in pixel, + rows:number,cols:number,level?:number, + node : { visual }, + port?, link?, + map?, + connections?} + */ +simu.prototype.createMeshNetwork = function (model) { + var self=this, + options=model.world.meshgrid; + if (!options.node) return this.out('Incomplete meshgrid model!'); + var margin_x = options.node.visual.width/2, + margin_y = options.node.visual.height/2, + space_x = options.node.visual.width/2, + space_y = options.node.visual.height/2, + off_x = options.off_x||5, + off_y = options.off_y||5, + xn,yn,x,y,w,h,p,obj,obj2,link, + node,world,node1,node2,place,port,ports={}, + id,i,j,k,level=0,dim=2, + xMoff=0,yMoff=0; + + if (options.levels) dim=3; + if (!options.matrix) this.gui.level=this.gui.level.map(function (x,i) { + return i==0?1:0; + }); else this.gui.level=this.gui.level.map(function (x,i) { + return 1; + }); + this.gui.level[-1]=1; + + this.out('Creating meshgrid network with '+options.rows+ + ' rows and '+options.cols+ + ' columns on '+(options.levels?options.levels:1)+ + ' levels.'); + + this.options.x=options.cols; + this.options.y=options.rows||0; + this.options.z=options.levels||0; + + function makeRef(o) {return o}; + function vec2str(vec) { + if (self.options.z) return vec.join(','); + else if (self.options.y) return vec.slice(0,2).join(','); + else return vec[0] } + + for(level=0;level<(options.levels?options.levels:1);level++) { + if (options.matrix && options.matrix[level]) { + xMoff=options.matrix[level][0]; + yMoff=options.matrix[level][1]; + } + // 1. Nodes + for(j=0;j1) this.log('[SIM] Created node '+node.id); + xn=off_x+margin_x+i*(options.node.visual.width+space_x)+xMoff; + yn=off_y+margin_y+j*(options.node.visual.height+space_y)+yMoff; + this.gui.objects[id] = { visual:{ + x:xn, + y:yn, + shape:options.node.visual.shape||'rect', + w:options.node.visual.width, + h:options.node.visual.height, + fill:options.node.visual.fill, + line:options.node.visual.line, + level:level + }, + obj:makeRef(node), + id:id + } + } + } + + // 2. Link ports of nodes + if (options.port && options.port.place) { + for(i in this.world.nodes) { + node=this.world.nodes[i]; + id='node['+node.id+']'; + obj=this.gui.objects[id]; + place=options.port.place(node); + if (place && obj) { + for(j in place) { + xn=obj.visual.x+place[j].x-options.port.visual.width/2+obj.visual.w/2; + yn=obj.visual.y+place[j].y-options.port.visual.height/2+obj.visual.h/2; + id='port['+node.id+':'+place[j].id+']'; + if (!ports[node.id]) ports[node.id]={}; + obj2={node:makeRef(node)}; + ports[node.id]['DIR.'+place[j].id]=obj2; + this.gui.objects[id] = { visual:{ + x:xn, + y:yn, + shape:options.port.visual.shape||'rect', + w:options.port.visual.width, + h:options.port.visual.height, + fill:options.port.visual.fill, + line:options.port.visual.line, + level:obj.visual.level + }, + obj:obj2, + id:id + } + } + + } + } + } + // 3. Links between nodes + // Only nodes having ports can be connected by links! + if (options.link) { + function port(i,j,k,dir) { + var p=ports[vec2str([i,j,k])]; + if (!p) return false; + return p[dir]; + } + function setPort(i,j,k,dir,link) { + var p=ports[vec2str([i,j,k])]; + if (!p) return ; + p[dir].link=link; + } + for(k=0;k<(options.levels?options.levels:1);k++) { + if (options.matrix && options.matrix[k]) { + xMoff=options.matrix[k][0]; + yMoff=options.matrix[k][1]; + } + for(j=0;j1) this.log('[SIM] Created node '+node.id); + id='node['+id+']'; + this.gui.objects[id] = {visual:{ + x:margin_x+off_x+xn+space_x*1.5-4, + y:margin_y+off_y+yn+space_y*1.5-4, + shape:'rect', + w:margin_x, + h:margin_y, + fill:(options.world&&options.world.visual&&options.world.visual.fill)||{color:'black'}, + line:(options.world&&options.world.visual&&options.world.visual.line)||{width:2} + },obj:this.world,id:id}; + id='world-anchor'; + this.gui.objects[id] = {visual:{ + x:off_x+1, + y:off_y+1, + shape:'rect', + w:margin_x+xn+space_x*1.5, + h:margin_y+yn+space_y*1.5, + line:(options.world&&options.world.visual&&options.world.visual.line)||{width:1,color:'#BBB'} + },obj:this.world,id:id}; + } +} + +/** Create a node in the simulation world + * + */ +simu.prototype.createNode = function (desc,drawnow) { + var self=this,id,index, + node,nodeid,port,pos,drawpos,xn,yn,x,y,p,options,chan,addr; + + desc.id=desc.id.replace(/\$WORLD/,this.world.id); + + function makeRef(x) {return x}; + if (desc.id == undefined) + throw 'createNpde: Invalid node descriptor, missing id attribute'; + if (desc.gps) { + pos=self.world.units.gps2px(desc.gps); + desc.x=pos.x; desc.y=pos.y; desc.z=pos.z; + } else if (desc.geo) { + pos=self.world.units.m2px(desc.geo); + desc.x=pos.x; desc.y=pos.y; desc.z=pos.z; + } + if (desc.x == undefined || desc.y == undefined) + throw 'createNode: Invalid node descriptor, missing coordinates'; + + id = 'node['+desc.id+']'; + if (this.gui.objects[id]) + throw 'createNode: Node '+desc.id+' exists already!'; + pos = {x:desc.x,y:desc.y}; + // Patch world model? + if (desc.gps) pos.gps=desc.gps; + if (desc.geo) pos.geo=desc.geo; + + node=Aios.Node.Node({id:desc.id,position:pos,parameter:desc.parameter||{}}); + + nodeid=node.id; + if (desc.location) node.location=desc.location; + this.world.addNode(node); + if (this.verbose>1) this.log('[SIM] Created node '+node.id); + xn=this.world2draw(desc).x+checkOption(desc.visual.center && desc.visual.center.x,0); + yn=this.world2draw(desc).y+checkOption(desc.visual.center && desc.visual.center.y,0); + + if (Comp.obj.isString(desc.visual.width)) desc.visual.width=this.convertUnit(desc.visual.width); + if (Comp.obj.isString(desc.visual.height)) desc.visual.height=this.convertUnit(desc.visual.height); + if (Comp.obj.isString(desc.visual.radius)) desc.visual.width=desc.visual.height=this.convertUnit(desc.visual.radius)*2; + this.gui.objects[id] = { + visual:{ + x:xn, + y:yn, + shape:desc.visual.shape||'rect', + icon:desc.visual.icon, + w:desc.visual.width, + h:desc.visual.height, + fill:desc.visual.fill, + line:desc.visual.line, + align:desc.visual.shape=='circle'?'center':desc.visual.align, + }, + obj:makeRef(node), + id:id + }; + + if (desc.visual.label) { + self.gui.objects['label['+desc.id+']'] = { + visual:{ + x:xn, + y:yn-5-(desc.visual.label.fontSize||24), + shape:'text', + fontSize:desc.visual.label.fontSize||24, + color:desc.visual.label.color, + text:desc.visual.label.text||desc.visual.label, + },id:'label['+desc.id+']' + } + if (drawnow) this.gui.drawObject('label['+desc.id+']'); + } + if (drawnow) this.gui.drawObject(id); + + + // Node virtual and physical communication ports + if (desc.port) { + // backward compatibility + if (Comp.obj.isArray(desc.port)) + desc.ports = desc.port; + else + desc.ports = [desc.port]; + } + if (desc.ports) for (index in desc.ports) { + port=desc.ports[index]; + if (!port) continue; + // Create a virtual link port object + switch (port.type) { + case 'multicast': + chan = (function (port) { + var chan = { + _connected:[], + _count:0, + _state:true, + + count:function () {return chan._count}, + /** Send agent snapshot to destination node. + * 'dest' must be a node identifier! + */ + /* OLDCOMM send: function (data,dest,current) { */ + /* NEWCOMM */ + send: function (msg) { + var node; + //console.debug('chan.vc.send',msg) + if (!chan.status()) return; + if (Comp.array.contains(chan._connected,msg.to.node /*dest*/)) { + node=self.world.getNode(msg.to.node /*dest*/); + if (!node) return; + //console.debug('chan.vc.send',msg,node) + if (msg.agent) + node.receive(msg.agent /*data*/); + else if (msg.signal) + node.handle(msg.signal); + chan._count += ((msg.agent||msg.signal).length||1); + if (msg.context) msg.context.kill=true; + } else { + // Throw exception?? + if (msg.context) msg.context.kill=true; + } + }, + // Enable or disable communication port (default: on) + getState: function () { return chan._state }, + label:desc.id+(port.id?(':'+port.id):''), + setState: function (on) { chan._state=on }, + /** Check connection status and/or return available connected or reachable nodes (links). + * + */ + status: function (path) { + // self.log('status state='+chan._state); + if (!chan._state) { + chan._connected=[]; + return none; + } + // Find all overlapping connections + var port1='port['+node.id+']',objs; + // self.log(port1); + if (port1) { + objs=self.gui.overlapObjects(port1); + } + // self.log(objs); + if (port.status) objs=port.status(objs); + if (objs && objs.length>0) { + // Return all connected nodes/links + chan._connected=Comp.array.map(objs,function (o) { + return o.replace(/port\[([^\]]+)\]/,'$1'); + }); + return chan._connected; + } else { + chan._connected=[]; + return none; + } + }, + virtual:true + }; return chan})(port); + // TODO multiple ports + node.connections.path=chan; + x=xn; y=yn; + id='port['+chan.label+']'; + if (!port.visual) port.visual={ + w:desc.visual.width*2, + h:desc.visual.height*2 + }; + if (port.visual.line && !port.visual.line.width) + port.visual.line.width=2; + port.visual.x=x+desc.visual.width/2; + port.visual.y=y+desc.visual.height/2; + if (Comp.obj.isString(port.visual.width)) + port.visual.width=self.convertUnit(port.visual.width); + if (Comp.obj.isString(port.visual.height)) + port.visual.height=self.convertUnit(port.visual.height); + if (Comp.obj.isString(port.visual.radius)) + port.visual.width= + port.visual.height=self.convertUnit(port.visual.radius)*2; + if (Comp.obj.isNumber(port.visual.radius)) + port.visual.width= + port.visual.height=port.visual.radius*2; + self.gui.objects[id] = {visual:{ + x:port.visual.x, + y:port.visual.y, + shape:port.visual.shape||'circle', + w:port.visual.width, + h:port.visual.height, + fill:port.visual.fill, + line:port.visual.line + },obj:chan,id:id} + if (drawnow) this.gui.drawObject(id); + break; + + case 'physical': + // Physical communication ports + options={verbose:1,proto:'http',multicast:true}; + if (port.verbose!=undefined) options.verbose=port.verbose; + if (port.proto != undefined) options.proto=port.proto; + if (port.multicast != undefined) options.multicast=port.multicast; + if (port.broker) options.broker=port.broker; + if (options.broker) options.multicast=false; + + if (port.port) addr=port.ip+':'+port.port; else addr=port.ip; + options.from=addr; + this.log('[SIM] Creating physical port IP('+addr+') '+Io.inspect(options)+' on node '+node.id); + chan=this.world.connectPhy( + Aios.DIR.IP(addr), + node, + { + broker:options.broker, + multicast:options.multicast, + name:options.name, + on:options.on, + oneway:options.oneway, + proto:options.proto, + rcv:options.from, + snd:options.to, + verbose:options.verbose + }); + chan.init(); + chan.start(); + node.connections.ip.chan=chan; + if (port.to) this.world.connectTo(Aios.DIR.IP(port.to),node); + break; + } + + } + if (this.verbose>1) this.log('[SIM] Created node '+node.id); + return nodeid; +} +/** Create a regular (or irregular) 2d patch grid of nodes and connections. + * type of options = { + width: number as node width in pixel, + height: number as node heigth in pixel, + visual?, // if not set, model.patches.default is required + rows:number,cols:number, + floating?:boolean, // if false/undef. grid consists of logical nodes, else resources + } + */ +simu.prototype.createPatchWorld = function (model,resources) { + var self=this, + i,j,row, + options=model.world.patchgrid, + patch = options.visual || (model.patches && model.patches.default && model.patches.default.visual); + if (!model.nodes || !model.nodes.world) + this.err('Incomplete patchgrid model (requires model.nodes.world); no world node found'); + + if (patch) options.visual = patch; + + var width = (options.visual&&options.visual.width)||options.width||10, + height = (options.visual&&options.visual.height)||options.height||10, + margin_x = 0, + margin_y = 0, + off_x = 0, + off_y = 0, + xn,yn,x,y,w,h,p,obj,obj2,link, + node,world,node1,node2,place,port,ports={}, + id,i,j,k,level=0,dim=2, + xMoff=0,yMoff=0; + + function makeRef(o) {return o}; + function vec2str(vec) { return vec.join(',') } + + this.out('Creating patchgrid network with '+options.rows+ + ' rows and '+options.cols+ + ' columns'); + + this.options.x=options.cols; + this.options.y=options.rows; + + // A world map for fast search of physical agents(nodes) + this.agentMap = []; + // A world map for fast search of physical resources + this.resourceMap = []; + + for (j=0;j1) this.log('[SIM] Created node '+node.id); + this.world.addNode(node); + } // else patch == resource + if (options.floating) id = 'patch['+id+']'; else id='node['+id+']'; + xn=off_x+margin_x+i*width+xMoff; + yn=off_y+margin_y+j*height+yMoff; + this.gui.objects[id] = { visual:{ + x:xn, + y:yn, + shape:options.visual.shape||'rect', + w:width, + h:height, + fill:options.visual.fill, + line:options.visual.line, + }, + obj:{parameter:{}}, + id:id + } + } + } + } else { + // Default patchgrid visual: frame boundary + grid pattern + xn=off_x+margin_x+xMoff; + yn=off_y+margin_y+yMoff; + id='patch[world]'; + this.gui.objects[id] = { visual:{ + x:xn, + y:yn, + shape:'rect', + pattern:{ + shape:'rect', + w:options.width, + h:options.width, + color:"1 #ddd 0.9" + }, + w:width*options.cols, + h:height*options.rows, + fill:null, + line:{color:'#888'}, + }, + obj:{parameter:{},patches:self.patches}, + id:id + } + // Draw resources before any other objects + this.createResources(resources); + + } + +} + + +/** Create a physical communication port + * + */ +simu.prototype.createPort = function (dir,options,nodeid) { + if (!options) options={}; + var multicast=options.multicast||true; + if (dir.tag != Aios.DIR.tag.IP) {} + if (options.from==undefined && dir.ip) options.from=dir.ip.toString(); + var chan=this.world.connectPhy( + dir, + this.getNode(nodeid), + { + broker:options.broker, + multicast:multicast, + name:options.name, + on:options.on, + oneway:options.oneway, + proto:options.proto||'udp', + rcv:options.from, + snd:options.to, + verbose:options.verbose||this.verbose + }); + chan.init(); + chan.start(); + return chan; +} + +/** Create unit conversion functions from a map object + * + */ +simu.prototype.createUnits = function (map) { + /* + ** 0---x latitude Y + ** | PX | GPS | GEO + ** | GUI | | + ** y +----- longitude 0-----X + */ + // Geographic units using map parameters {offset,scale} + this.world.units.gps2px = function (gps) { + if (!gps.delta) { + if (!map.area) area={x:0,y:0}; + else if (!map.area.selector) area={x:map.area.x||0,y:map.area.y||0}; + else area=map.area[map.area.selector(gps)]; + return { + x:area.x+((gps.longitude-map.longitude.offset)*map.longitude.scale)|0, + y:area.y-((gps.latitude-map.latitude.offset)*map.latitude.scale)|0, + z:gps.height*(map.height?map.height.scale:1) + }; + } else + // delta coord. + return { + x:(gps.longitude*map.longitude.scale)|0, + y:-(gps.latitude*map.latitude.scale)|0, + z:gps.height*(map.height?map.height.scale:1) + }; + }; + this.world.units.px2gps = function (px) { + if (!px.delta) { + if (!map.area) area={x:0,y:0}; + else if (!map.area.selector) area={x:map.area.x||0,y:map.area.y||0}; + else area=map.area[map.area.selector(px)]; + return { + latitude: -(px.y-area.y)/map.longitude.scale+map.longitude.offset, + longitude: (px.x-area.x)/map.latitude.scale+map.latitude.offset + } + } else + // delta; + return { + latitude: -px.y/map.longitude.scale, + longitude: px.x/map.latitude.scale + } + }; + this.world.units.gps2geo = function (gps) { + // Only for delta vectors + if (gps.delta) return { + X:gps.longitude*111320, + Y:gps.latitude*111320, + Z:gps.height + } + }, + this.world.units.geo2gps = function (geo) { + // Only for delta vectors + if (geo.delta) return { + latitude: geo.Y/113200, + longitude: geo.X/113200, + height: geo.Z + } + }; + + // Geometric units using map parameters {offset,scale} + this.world.units.geo2px = function (geo) { + if (geo.delta) return { + x:(geo.X)*(map.X.scale||1), + y:(geo.Y)*(map.Y.scale||1), + z:(geo.Z)*(map.Z.scale||1) + }; else return { + x:(geo.X-map.X.offset||0)*(map.X.scale||1), + y:(geo.Y-map.Y.offset||0)*(map.Y.scale||1), + z:(geo.Z-map.Z.offset||0)*(map.Z.scale||1) + } + }; + this.world.units.px2geo = function (px) { + if (px.delta) return { + X:px.x/(map.X.scale||1), + Y:px.y/(map.Y.scale||1), + Z:px.z/(map.Z.scale||1) + }; else return { + X:px.x/(map.X.scale||1)+(map.X.offset||0), + Y:px.y/(map.Y.scale||1)+(map.Y.offset||0), + Z:px.z/(map.Z.scale||1)+(map.Z.offset||0) + } + }; + function rad2deg (rad) { return rad/Math.PI*180.0 }; + function deg2rad (deg) { return deg/180.0*Math.PI }; + + // Computer distance and angle direction between two GPS locations + this.world.units.gps2dist = function (gps1,gps2) { + var dy=(gps2.latitude-gps1.latitude)*111320, + dx=(gps2.longitude-gps1.longitude)*111320, + dz=(gps2.height-gps1.height), + angle = rad2deg(Math.atan2(dy, dx)); + // Convert to 0\B0: N, 90\B0 : E, 180\B0 : S, 270\B0 : W + if (angle >=0 && angle <= 180) angle=270-angle; + else if(angle > -90) angle=-angle+270; + else angle=-angle-90; + + return {dx:dx,dy:dy,dz:dz,angle:angle} + } + + this.world.units.rad2deg = rad2deg; + this.world.units.deg2rad = deg2rad; + this.aiosX.units=this.world.units; +} + +// Draw resources +simu.prototype.createResources = function (resources) { + var self=this,xi,yi,wi,hi; + if (resources.length) this.log('Creating '+resources.length+' resource(s)..'); + Comp.array.iter(resources,function (r) { + if (r.id != undefined && r.visual) { + id='resource['+r.id+']'; + self.gui.objects[id] = { + visual:{ + x:r.visual.x, + y:r.visual.y, + shape:r.visual.shape||'circle', + w:r.visual.width, + h:r.visual.height, + fill:r.visual.fill, + line:r.visual.line + },obj:r.data,id:id + } + if (r.data) self.gui.objects[id].data=r.data; + if (r.class) self.gui.objects[id].class=r.class; + if (self.options.patch) { + xi=int(r.visual.x/self.options.patch.width); + yi=int(r.visual.y/self.options.patch.height); + wi=int(r.visual.width/self.options.patch.width); + hi=int(r.visual.height/self.options.patch.height); + if (yi>self.options.patch.rows || xi>self.options.patch.cols) + throw Error('createResources: invalid visual patch coordinates ('+xi+','+yi+')'); + self.resourceMap[yi][xi][id]=self.gui.objects[id]; + self.rtree.insert({x:xi, y:yi, w:wi, h:hi},id); + } + id='label['+r.id+']'; + if (r.visual.label) self.gui.objects[id] = { + visual:{ + x:r.visual.x+10, + y:r.visual.y+10, + shape:'text', + fontSize:r.visual.label.fontSize||24, + color:r.visual.label.color, + text:r.visual.label.text||r.visual.label, + },id:id + } + } + }); +} + + +/** Create the simulation world + * + * type of model = { name, classes, resources?, world} + * type of world = { init?, map?, nodes?, meshgrid?, patchgrid?, resources?, units? } + * type of units = { map?, gps2px?, px2gps?, geo2px?, px2gep? } + * + * + * + */ +simu.prototype.createWorld = function (model) { + var margin_x = 5, + margin_y = 5, + off_x = 5, + off_y = 5, + node, + self=this, + id,i,j,p,res,pos, + map,area; + self.model=model; + try { + // Check model .. + if (model.agents) { + for (p in model.agents) { + if (!model.agents[p].visual) throw "Invalid model.agents."+p+": missing visual"; + if (res=this.checkVisual(model.agents[p].visual)) throw "Invalid model.agents."+p+".visual: "+res+")"; + } + } + + + + if (model.world.units) this.world.units=model.world.units; + if (model.world.units && model.world.units.map) this.createUnits(model.world.units.map); + + // Draw resources before any other objects (lowest z level) + // model.world.resources returns array of simulation object descriptors + var resources = model.world.resources?model.world.resources(model):[]; + + if (!Comp.obj.isArray(resources)) + throw ('odel.world.resources returned '+(typeof resources)+', expected array'); + + if (!model.world.patchgrid) this.createResources(resources); + if (model.world.meshgrid) this.createMeshNetwork(model); + if (model.world.patchgrid) this.createPatchWorld(model,resources); + if (model.world.nodes && typeof model.world.nodes == "function") + model.world.map=model.world.nodes; // synonym + if (model.world.map) { + // use a mapping function and node descriptors {id,x,y,link,visual} + Comp.array.iter(model.world.map(model), function (desc,i) { + self.createNode(desc); + }); + } + } catch (e) { + this.log('Failure in createWorld: '+e); + if (this.gui.options.verbose) this.log(e.stack); + } +} + +// Delete a JAM node and visuals +simu.prototype.deleteNode = function (nodeid) { try { + var objid = 'node['+nodeid+']', + node = this.world.getNode(nodeid), + objs,p,port; + + this.gui.destroyObject(objid); + // Remove ports of deleted node + if (node) { + for(p in {NORTH:1,SOUTH:1,WEST:1,EAST:1,UP:1,DOWN:1}) { + if (node.connections[p.toLowerCase()]) + this.gui.destroyObject('port['+nodeid+':'+p+']'); + } + if (node.connections.path) { + this.gui.destroyObject('port['+node.connections.path.label+']'); + } + if (node.connections.ip && node.connections.ip.chann) { + // Stop physical link + node.connections.ip.chann.stop(); + } + this.world.removeNode(nodeid); + node.destroy(); + } else if (this.gui.options.verbose) this.log('deleteNode: No node found for id '+nodeid); + + // Remove links to deleted node + objs=this.gui.findObjects('link',nodeid); + // console.debug('deleteNode',objid,node,objs) + for(p in objs) { + this.gui.destroyObject(p); + } + // Cleanup links of ports currently linked with other nodes + objs=this.gui.findObjects('port','.+'); + for(p in objs) { + port=objs[p].obj; + if (port.link && + ((port.link.node1 && port.link.node1.id==nodeid)|| + (port.link.node2 && port.link.node2.id==nodeid))) port.link=undefined; + } + if (this.verbose>1) this.log('[SIM] Deleted node '+nodeid); + } catch (e) { console.log(e) } +} + +/** Destroy the simulation world: Destroy agents, AIOS nodes, and AIOS world. + * + */ +simu.prototype.destroy = function () { + var n,p; + for(n in this.world.nodes) { + if (!this.world.nodes[n]) continue; // ?? + this.gui.objects[this.world.nodes[n].id]=undefined; + if (this.world.nodes[n].connections.ip) this.world.nodes[n].connections.ip.chan.stop(); + this.world.nodes[n].destroy(); + } + if (this.model.world && this.model.world.init && this.model.world.init.physics) { + if (this.simuPhy) + this.gui.destroySimuPhy(); + this.simuPhy=none; + } + this.world.nodes=none; + this.world=none; + this.model=none; + if (this.Db) for(p in this.Db) this.Db[p].close(); + this.Db=none; + this.log('[SIM] Simulation world destroyed'); +} +/** Return agent object (or array if ais is a regex) referenced by logical node number, position, or name, + ** and agent identifier. + * If @id is undefined return current node object. + */ +simu.prototype.getAgent = function (id,aid) { + var node = this.getNode(id); + if (node) return node.getAgent(aid); +} +simu.prototype.getAgentProcess = function (id,aid) { + var node = this.getNode(id); + if (node) return node.getAgentProcess(aid); +} + + +/** Return node object referenced by logical node number, position, or name + * If @id is undefined return current node object. + */ +simu.prototype.getNode = function (id) { + var node; + if (id==undefined) return this.world.nodes[0]; + if (typeof id == 'number') + node=this.world.nodes[id]; + else if (typeof id == 'string') { + // Search node identifier; + node = this.world.getNode(id); + } else if (id.x != undefined && + id.y != undefined) { + // Search node position; + loop: for(var i in this.world.nodes) { + if (this.world.nodes[i] && Comp.obj.equal(this.world.nodes[i].position,id)) { + node = this.world.nodes[i]; + break loop; + } + } + } + + return node; +} + +/** Return process object referenced by logical node number, position, or name, + ** and agent identifier. + * If @id is undefined return current node object. + */ +simu.prototype.getProcess = function (id,aid) { + var node = this.getNode(id); + if (node) return node.getAgentProcess(aid); +} + +/** Return statistics + * type stats = {steps,time,agents:{},agentsN:{},all;number} + */ + +simu.prototype.getStats = function () { + return this.aiosX.getStats(); +} +/** Initialize and create simulation world + * + */ +simu.prototype.init = function (model) { + var a,n,c,params,proc,count; + try { + this.out('Initializing ...'); + + if (model.parameter) this.parameter=model.parameter; + // 1. AC COMPILING + if (model.agents) model.classes = model.agents; // synonym for classes + else if (model.classes) model.agents = model.classes; + if (model.classes) + for(c in model.classes) { + if (!model.classes[c].behaviour) + this.out('Error: Agent class "'+c+'" has no behaviour!'); + else { + this.out('Compiling agent class template "'+c+'"...'); + this.addClass(model.classes[c].behaviour,c,model.classes[c].visual); + } + } + // 2. CREATE World + + this.createWorld(model); + + // 3. START AGENTS + if (model.world && model.world.init && model.world.init.agents) { + for (a in model.world.init.agents) { + if (!Comp.obj.isFunction(model.world.init.agents[a])) + this.out('Error: Init: "'+a+'" is not a function!'); + else if (!this.world.classes[a]) + this.out('Error: Init: Agent class "'+a+'" is not registered!'); + else { + this.out('Creating agents of class "'+a+'" ..'); + count=0; + for(n in this.world.nodes) { + params=model.world.init.agents[a](this.world.nodes[n].id, + this.world.nodes[n].position, + // can be defined in model.world.nodes(desc) + this.world.nodes[n].options.parameter); + if (params) { + if (params.level == undefined) params.level=1; + else if (params.level<0 || params.level>3) + this.out('Error: Init: Invalid agent level '+params.level+' for agent class "'+a+'".'); + else { + proc=Aios.Code.createOn(this.world.nodes[n], + this.world.classes[a][params.level], + params.args,params.level,a); + count++; + } + } + } + this.out('Created '+count+' '+a+' agents.'); + } + } + } + + // 4. CREATE PHYSICAL WOLRD - IF ANY + if (model.world.init && model.world.init.physics) { + if (!this.simuPhy) + this.simuPhy=this.gui.createSimuPhy(); + model.world.init.physics(this.simuPhy); + } + this.out('Ready.'); + } catch (e) { + this.out('Error: '+e); + if (this.verbose) this.out(Io.sprintstack(e)); + } +} + +/** Move an object (node or resource) relative to its current position. + * type of dx,dy,dz = number is normalized pixel units | string (number + 'm' | '\B0') + * + * + */ +simu.prototype.moveObject = function(objid,dx,dy,dz) { + var id,obj,_dx,_dy,_dz,isnode=false; + if (dx && Comp.obj.isString(dx)) { + if (Comp.string.endsWith(dx,'\B0') && this.world.units.gps2px) + _dx=this.world.units.gps2px({delta:true,longitude:Number(Comp.string.prefix(dx,'\B0'))}).x; + else if (Comp.string.endsWith(dx,'m') && this.world.units.geo2px) + _dx=this.world.units.geo2px({delta:true,X:Number(Comp.string.prefix(dx,'m'))}).x; + else + _dx=Number(dx); + } else _dx=dx; + if (dy && Comp.obj.isString(dy)) { + if (Comp.string.endsWith(dy,'\B0') && this.world.units.gps2px) + _dy=this.world.units.gps2px({delta:true,latitude:Number(Comp.string.prefix(dy,'\B0'))}).y; + else if (Comp.string.endsWith(dy,'m') && this.world.units.geo2px) + _dy=this.world.units.geo2px({delta:true,Y:Number(Comp.string.prefix(dx,'m'))}).y; + else + _dy=Number(dy); + } else _dy=dy; + if (dz && Comp.obj.isString(dz)) { + if (Comp.string.endsWith(dz,'m') && this.world.units.geo2px) + _dz=this.world.units.geo2px({delta:true,Z:Number(Comp.string.prefix(dz,'m'))}).z; + else + _dz=Number(dz); + } else _dz=dz; + + if (Comp.string.startsWith(objid,'node')) { + id=objid.replace(/node\[([^\]]+)\]/,'$1'); + obj=this.world.getNode(id); + isnode=true; + } else if (Comp.string.startsWith(objid,'resource')) { + // TODO, use .data + } + if (!obj) return; + + if (obj && obj.position) { + // Assumption: x/y/z are normalized pixel and linear coordinates! + if (obj.position.x != undefined && _dx) obj.position.x += _dx; + if (obj.position.y != undefined && _dy) obj.position.y += _dy; + if (obj.position.z != undefined && _dz) obj.position.z += _dz; + if (obj.position.gps && this.world.units.px2gps) { + if (_dx) obj.position.gps.latitude += this.world.units.px2gps({delta:true,y:_dy}).latitude; + if (_dy) obj.position.gps.longitude += this.world.units.px2gps({delta:true,x:_dx}).longitude; + if (_dz) obj.position.gps.height += this.world.units.px2gps({delta:true,z:_dz}).height; + } + } + if (isnode) this.gui.moveObject(objid,_dx,_dy,'agent','port'); + else this.gui.moveObject(objid,_dx,_dy); +} + +/** Move an object (node or resource) to new absolute position. + * type of x,y,z = number is normalized pixel unit | string (number + 'm' | '\B0') + * + * + */ +simu.prototype.moveObjectTo = function(objid,x,y,z) { + var id,obj,_px,_gps,isnode=false; + if (Comp.obj.isString(x) && Comp.obj.isString(y)) { + if (Comp.string.endsWith(x,'\B0') && Comp.string.endsWith(x,'\B0') && this.world.units.gps2px) { + _gps={latitude:Number(Comp.string.prefix(x,'\B0')),longitude:Number(Comp.string.prefix(y,'\B0')),height:Number(z)}; + _px=this.world.units.gps2px(_gps); + } else if (Comp.string.endsWith(x,'m') && Comp.string.endsWith(y,'m') && this.world.units.geo2px) + _px=this.world.units.geo2px({X:Number(Comp.string.prefix(x,'m')),Y:Number(Comp.string.prefix(y,'m')),Z:Number(z)}); + else + _px={x:Number(x),y:Number(y),z:Number(z)}; + } else _px={x:x,y:y,z:z}; + + if (Comp.string.startsWith(objid,'node')) { + id=objid.replace(/node\[([^\]]+)\]/,'$1'); + obj=this.world.getNode(id); + isnode=true; + } else if (Comp.string.startsWith(objid,'resource')) { + // TODO, use .data + } + if (!obj) return; + + if (obj && obj.position && !this.options.patch) { + if (obj.position.x != undefined) obj.position.x = _px.x; + if (obj.position.y != undefined) obj.position.y = _px.y; + if (obj.position.z != undefined) obj.position.z = _px.z; + if (obj.position.gps && _gps) { + obj.position.gps.latitude = _gps.latitude; + obj.position.gps.longitude = _gps.longitude; + obj.position.gps.height = _gps.height; + } + } + if (isnode) this.gui.moveObjectTo(objid,_px.x,_px.y,'agent','port'); + else this.gui.moveObjectTo(objid,_px.x,_px.y); +} + +// Set simulation delay between two steps +simu.prototype.setDelay = function (ms) { + this.delay=ms; +} + +/** Set simulation parameter (accessible by agents using simu.paramater(name)) + * + */ +simu.prototype.setParameter = function (name,value) { + this.parameter[name]=value; +} + +/** Run or step simulation + */ +simu.prototype.simulate = function (steps,callback) { + var self=this, + realtime=this.options.time=='real', + milliTime=function () {return Math.ceil(Date.now())}, + start,stop,timeReal, + lazy=this.gui.options.display.lazy,suspend=false, + curtime=realtime?milliTime():Aios.time(); + var lasttime=curtime; + this.stepped=steps; + + start=curtime; + if (realtime && this.time>0) current.world.lag=current.world.lag+(start-this.time); + else if (this.time>0) this.lag=this.lag+(start-this.time); + this.time0=start; + this.time=start; + this.run=true; + if (this.verbose) this.log('[SIM] Stepping simulation ('+steps+') at '+this.step+ ' .. (t '+(start-current.world.lag-this.lag)+' ms)'); + function starting () { + if (self.model && self.model.world && self.model.world.start) try { self.model.world.start({ + log:self.log.bind(self), + time:Date.now(), + step:self.step, + simtime:curtime, + model:self.model, + cpu:self.stats.cpu, + }) } catch (e) { self.gui.log('model.world.start: '+e.toString())}; + } + function stopping () { + if (self.gui.worldWin.container && suspend) { // enable world win updates + suspend=false; self.gui.worldWin.container.resume() }; + self.gui.updateGui(true); + if (self.model && self.model.world && self.model.world.stop) try { self.model.world.stop({ + log:self.log.bind(self), + time:Date.now(), + step:self.step, + simtime:curtime, + model:self.model, + cpu:self.stats.cpu, + }) } catch (e) { self.gui.log('model.world.stop:'+e.toString())}; + } + // self.but2.setStyle({bg:'red'}); + Aios.config({iterations:1}); + var loop = function () { + if (self.gui.worldWin.container && !suspend) { // disable world win updates + suspend=true; self.gui.worldWin.container.suspend() }; + + // Execute JAM scheduler + timeReal=Date.now(); + + var nexttime=Aios.scheduler(); + + self.stats.cpu += (Date.now()-timeReal); + + if (self.gui.worldWin.container && !lazy && suspend) { // enable world win updates + suspend=false; self.gui.worldWin.container.resume() }; + + if (self.gui.logging) self.gui.logTable[self.step]=self.getStats(); + if (self.cleanWorld) self.cleanWorld=self.gui.cleanWorld()>0; + self.step++; + if (self.stepped>0) self.stepped--; + + self.time=curtime=Aios.time(); + if (!self.run) { + stop=realtime?milliTime():Aios.time(); + //self.log(stop+','+current.world.lag+','+self.lag+','+curtime+','+milliTime()); + if (self.verbose) self.log('[SIM] Stopping simulation at '+self.step+ ' .. (d '+(stop-self.time0)+' ms / t '+(stop-current.world.lag-self.lag)+' ms): Stopped.'); + self.time=curtime; + if (callback) callback(); + stopping(); + return; + } + if (self.stepped==0) { + stop=realtime?milliTime():Aios.time(); + self.run=false; + //self.but2.setStyle({bg:'blue'}); + if (self.verbose) self.log('[SIM] Stopping simulation at '+self.step+ ' .. (d '+(stop-self.time0)+' ms / t '+(stop-current.world.lag-self.lag)+' ms): No more steps.'); + self.time=stop; + if (callback) callback(); + stopping(); + return; + } + if (nexttime>0) self.loop=setTimeout(loop,(self.options.time=='real'?nexttime:self.delay)); // TODO: resonable timeout if nexttime is in steps!!! + else if (nexttime<0 && !self.delay) self.loop=setImmediate(loop); + else if (nexttime<0) self.loop=setTimeout(loop,self.delay); + else { + // self.but2.setStyle({bg:'blue'}); + stop=realtime?milliTime():Aios.time(); + self.run=false; + if (self.verbose) self.log('[SIM] Stopping simulation at '+self.step+ ' .. (d '+(stop-self.time0)+' ms / t '+(stop-current.world.lag-self.lag)+' ms): Nothing to run.'); + self.time=curtime; + if (callback) callback(); + stopping(); + return; + } + + if ((curtime-lasttime)>100) { + if (self.gui.worldWin.container && lazy && suspend) { // enable world win updates + suspend=false; self.gui.worldWin.container.resume() }; + self.gui.updateGui(true); + lasttime=curtime; + } else + self.gui.updateGui(false); + } + starting(); + this.loop = setTimeout(loop,0); +} + +/** Start simulation + * steps: number of simulation steps + * temp: temporary step + * + */ +simu.prototype.start = function (steps,temp) { + var self=this; + if (steps<=0 || (temp && this.stopped)) return; + if (!temp) { + this.stopped=false; + this.gui.UI('simulationWinButStep').disable(); + this.gui.UI('simulationWinButPlay').disable(); + this.gui.UI('simulationWinButStop').enable(); + } + setImmediate(function () { + self.simulate(self.stepped|steps,function () { + if (self.stepped==0) { + this.stopped=true; + self.gui.UI('simulationWinButStep').enable(); + self.gui.UI('simulationWinButPlay').enable(); + self.gui.UI('simulationWinButStop').disable(); + } + }); + }); +} + +/** Stop simulation, return remaining steps (only in temporary mode) + * + */ +simu.prototype.stop = function (temp) { + this.run=false; + if (this.loop) clearTimeout(this.loop); + this.loop=null; + if (this.gui.worldWin.container) this.gui.worldWin.container.resume() + if (temp && !this.stopped) + return this.stepped-1; + else { + this.stepped=0; + this.stopped=true; + return 0; + } +} + +/** convert simulation world to graphics coordinates + */ +simu.prototype.world2draw = function (p) { + if (this.options.patch) return {x:p.x*this.options.patch.width, + y:p.y*this.options.patch.height}; + else return p; +} + +var Simu = function(options) { + var obj=none; + obj = new simu(options); + return obj; +} + +module.exports = { + simu:simu, + Simu:Simu, + current:function (module) { current=module.current; Aios=module; JamAnal.current(module)} +}