/** * @classdesc The main Snaptobus class * @author Nike * @version 1.0 */ class Snaptobus{ constructor(options){ this.spriteDefs = options.spriteDefs this._curBusConfig = [] this._stagedBusConfig = options.busConfig this.snap = options.snap this.commitConfig() } get busConfig() { return this._stagedBusConfig } get liveBusConfig() { return this._curBusConfig } set busConfig(newConfig) { this._stagedBusConfig = newConfig } async commitConfig(){ const chansToAdd = [] const chansToKeep = [] for(const chanObj of this._stagedBusConfig){ console.log('staged chan:',chanObj.chan,' current ones:', this._curBusConfig.map(item => item.chan)) if(this._curBusConfig.map(item => item.chan).includes(chanObj.chan)) chansToKeep.push(chanObj) else chansToAdd.push(chanObj) } const chansToDel = this._curBusConfig.filter(item => (!chansToKeep.map(c=>c.chan).includes(item.chan) && !chansToAdd.map(c=>c.chan).includes(item.chan))) await app.MessageBus.subscribe(chansToAdd.map(item => item.chan)) await app.MessageBus.unSubscribe(chansToDel.map(item => item.chan)) // console.log('subscribe:', chansToAdd.map(item => item.chan)) // console.log('unSubscribe:', chansToDel) const eventsToAdd = chansToAdd.flatMap(item => item.events.map(ev => ({ chan:item.chan, eventName:ev.eventName }))) let eventsToDel = []//= chansToDel.flatMap(item => item.events.map(ev => ({ chan:item.chan, eventName:ev.eventName }))) for(const oldChan of this._curBusConfig){ for(const oldEvent of oldChan.events){ for(const keepChan of chansToKeep){ if(!keepChan.events.map(item=>item.eventName).includes(oldEvent.eventName)) eventsToDel.push({chan: oldChan.chan, eventName: oldEvent.eventName}) } } } // console.log('eventsToAdd:', eventsToAdd) // console.log('eventsToDel:', eventsToDel) for(const eventToAdd of eventsToAdd){ app.MessageBus.addBusListener(eventToAdd.eventName, [eventToAdd.chan], this.processBusEvent.bind(this, eventToAdd.eventName,), 'snaptobus') } for(const eventToDel of eventsToDel){ app.MessageBus.removeBusListener(eventToDel.eventName, this.processBusEvent.bind(this, eventToDel.eventName), 'snaptobus') } this._curBusConfig = this.deepClone(this._stagedBusConfig) } deepClone(obj) { // Needed because structuredClone doesn't take functions (and we have transformers) if (obj === null || typeof obj !== 'object') { return obj } if (Array.isArray(obj)) { return obj.map((el => this.deepClone(el))) } const clone = {} for (const key in obj) { clone[key] = this.deepClone(obj[key]) } return clone } observeObject(obj, onChange) { const wrap = (value) => (value && typeof value === 'object') ? this.observeObject(value, onChange) : value const handler = { set: (target, prop, value) => { const oldValue = target[prop] target[prop] = wrap(value) onChange(prop, value, oldValue, target) return true }, deleteProperty: (target, prop) => { const oldValue = target[prop] delete target[prop] onChange(prop, undefined, oldValue, target) return true } } // Walk initial keys/elements (construction time) for (const key of Object.keys(obj)) { obj[key] = wrap(obj[key]) } return new Proxy(obj, handler) } processBusEvent(eventType, chan, payload, userId, x){ const chanObj = this._curBusConfig.find(item => app.MessageBus.chanMatch(chan, item.chan)) if(!chanObj) return const eventObj = chanObj.events.find(item => item.eventName==eventType) if(!eventObj) return // assign attributes in payload to all snap objects for that eventType // each snap is an assignation of attributes via a selector for(const snapDef of eventObj.snaps){ const selector = snapDef.selector.replace(/\$\{(\w+)\}/g, (_, key) => this.getValueByPath(payload, key)) console.log('Will look for snap:', selector) const snaps = this.snap.selectAll(selector) console.log(`found ${snaps.length} snaps`) snaps.forEach(snapEl => { const newAttr = this.assignFromConfig(payload, snapDef.assign) if(snapDef.animate){ snapEl.animate( newAttr, 400, mina.linear ) } else { snapEl.attr(newAttr) } }) } } assignFromConfig(data, replaceDef) { console.log('assignFromConfig', data, replaceDef) const result = {} for (const [key, rule] of Object.entries(replaceDef)) { if (typeof rule === 'string') { // plain path result[key] = this.getValueByPath(data, rule) } else if((typeof(rule) == 'object') && (typeof(rule.transformer) == 'function')) { // transformer const fnargs = (rule.arguments || []).map(arg => this.getValueByPath(data,arg)) result[key] = rule.transformer(...fnargs) } } return result } getValueByPath(obj, path) { return(path.split('.').reduce((acc, key) => acc?.[key], obj)) } } /* const s2bConfig = [ { chan: 'gps:agents', // What to subscribe to events: [ // What to select on this chan { eventName: 'moving', snaps: [ { // selector will be used as css selector for a snap element / group, // with LAST MINUTE template resolving of event properties (with eventual dots) selector: '#${aid}', assign: { x: 'coords.x', // type string: event property, eventual dots to go down object y: 'coords.y', }, animate: true } ] }, { eventName: 'rotating', snaps: [ { selector: '#${aid}', assign: { r: 'rotangle' }, animate: true } ] }, ] }, { chan: 'agent:*', // wildcards allowed events: [ { eventName: 'aging', snaps: [ { selector: '#{aid}', assign: { fill: { // transformer function arguments: [ 'age' ], // What to give from the event as function's params transformer: i => `rgb(${Math.round(255 * i / 10)},0,${Math.round(255 * (1 - i / 10))})` }, }, } ] }, ] }, ] */ /* payload = {} eventType */