import debug from "debug"; import { Literal, NamedNode, Quad, Quad_Object, Quad_Predicate, Quad_Subject, Term } from "n3"; import { some } from "underscore"; import { Patch } from "../patch"; import { SyncedGraph } from "../SyncedGraph"; import { shortShow } from "../show_specific"; import { SubEvent } from "sub-events"; // todo: Align these names with newtypes.py, which uses HexColor and VTUnion. type Color = string; export type ControlValue = number | Color | NamedNode; const log = debug("effect"); function isUri(x: Term | number | string): x is NamedNode { return typeof x == "object" && x.termType == "NamedNode"; } // todo: eliminate this. address the scaling when we actually scale // stuff, instead of making a mess of every setting function valuePred(graph: SyncedGraph, attr: NamedNode): NamedNode { const U = graph.U(); const scaledAttributeTypes = [U(":color"), U(":brightness"), U(":uv")]; if (some(scaledAttributeTypes, (x: NamedNode) => attr.equals(x))) { return U(":value"); } else { return U(":value"); } } // also see resourcedisplay's version of this function effContext(graph: SyncedGraph, uri: NamedNode): NamedNode { return graph.Uri(uri.value.replace("light9.bigasterisk.com/effect", `light9.bigasterisk.com/show/${shortShow}/effect`)); } export function newEffect(graph: SyncedGraph): NamedNode { // wrong- this should be our editor's scratch effect, promoted to a // real one when you name it. const uri = graph.nextNumberedResource(graph.Uri("http://light9.bigasterisk.com/effect/effect")); const effect = new Effect(graph, uri); const U = graph.U(); const ctx = effContext(graph, uri); const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => graph.Quad(s, p, o, ctx); const addQuads = [ quad(uri, U("rdf:type"), U(":Effect")), quad(uri, U("rdfs:label"), graph.Literal(uri.value.replace(/.*\//, ""))), quad(uri, U(":publishAttr"), U(":strength")), quad(uri, U(":effectFunction"), U(":effectFunction/scale")), ]; const patch = new Patch([], addQuads); log("init new effect", patch); graph.applyAndSendPatch(patch); return effect.uri; } // effect settings data; r/w sync with the graph export class Effect { // :effect1 a Effect; :setting ?eset . ?eset :effectAttr :deviceSettings; :value ?dset . ?dset :device .. private eset?: NamedNode; private dsettings: Array<{ dset: NamedNode; device: NamedNode; deviceAttr: NamedNode; value: ControlValue }> = []; private ctxForEffect: NamedNode; settingsChanged: SubEvent = new SubEvent(); constructor(public graph: SyncedGraph, public uri: NamedNode) { this.ctxForEffect = effContext(this.graph, this.uri); graph.runHandler(this.rebuildSettingsFromGraph.bind(this), `effect sync ${uri.value}`); } private getExistingEset(): NamedNode | null { const U = this.graph.U(); for (let eset of this.graph.objects(this.uri, U(":setting"))) { if (this.graph.uriValue(eset as Quad_Subject, U(":effectAttr")).equals(U(":deviceSettings"))) { return eset as NamedNode; } } return null; } private getExistingEsetValueNode(): NamedNode | null { const U = this.graph.U(); const eset = this.getExistingEset(); if (eset === null) return null; try { return this.graph.uriValue(eset, U(":value")); } catch (e) { return null; } } private patchForANewEset(): { p: Patch; eset: NamedNode } { const U = this.graph.U(); const eset = this.graph.nextNumberedResource(U(":e_set")); return { eset: eset, p: new Patch( [], [ // new Quad(this.uri, U(":setting"), eset, this.ctxForEffect), new Quad(eset, U(":effectAttr"), U(":deviceSettings"), this.ctxForEffect), ] ), }; } private rebuildSettingsFromGraph(patch?: Patch) { const U = this.graph.U(); log("syncFromGraph", this.uri); // this repeats work- it gathers all settings when really some values changed (and we might even know about them). maybe push the value-fetching into a secnod phase of the run, and have the 1st phase drop out early const newSettings = []; const deviceSettingsNode = this.getExistingEsetValueNode(); if (deviceSettingsNode !== null) { for (let dset of Array.from(this.graph.objects(deviceSettingsNode, U(":setting"))) as NamedNode[]) { // // log(` setting ${setting.value}`); // if (!isUri(dset)) throw new Error(); let value: ControlValue; const device = this.graph.uriValue(dset, U(":device")); const deviceAttr = this.graph.uriValue(dset, U(":deviceAttr")); const pred = valuePred(this.graph, deviceAttr); try { value = this.graph.uriValue(dset, pred); if (!(value as NamedNode).id.match(/^http/)) { throw new Error("not uri"); } } catch (error) { try { value = this.graph.floatValue(dset, pred); } catch (error1) { value = this.graph.stringValue(dset, pred); // this may find multi values and throw } } // log(`change: graph contains ${deviceAttr.value} ${value}`); newSettings.push({ dset, device, deviceAttr, value }); } } this.dsettings = newSettings; log(`settings is rebuilt to length ${this.dsettings.length}`); this.settingsChanged.emit(); // maybe one emitter per dev+attr? // this.onValuesChanged(); } currentValue(device: NamedNode, deviceAttr: NamedNode): ControlValue | null { for (let s of this.dsettings) { if (device.equals(s.device) && deviceAttr.equals(s.deviceAttr)) { return s.value; } } return null; } // change this object now, but return the patch to be applied to the graph so it can be coalesced. edit(device: NamedNode, deviceAttr: NamedNode, newValue: ControlValue | null): Patch { log(`edit: value=${newValue}`); let existingSetting: NamedNode | null = null; let result = new Patch([], []); for (let s of this.dsettings) { if (device.equals(s.device) && deviceAttr.equals(s.deviceAttr)) { if (existingSetting !== null) { // this is corrupt. There was only supposed to be one setting per (dev,attr) pair. But we can fix it because we're going to update existingSetting to the user's requested value. log(`${this.uri.value} had two settings for ${device.value} - ${deviceAttr.value} - deleting ${s.dset}`); result = result.update(this.removeEffectSetting(s.dset)); } existingSetting = s.dset; } } if (newValue !== null && this.shouldBeStored(deviceAttr, newValue)) { if (existingSetting === null) { result = result.update(this.addEffectSetting(device, deviceAttr, newValue)); } else { result = result.update(this.patchExistingDevSetting(existingSetting, deviceAttr, newValue)); } } else { if (existingSetting !== null) { result = result.update(this.removeEffectSetting(existingSetting)); } } return result; } shouldBeStored(deviceAttr: NamedNode, value: ControlValue | null): boolean { // this is a bug for zoom=0, since collector will default it to // stick at the last setting if we don't explicitly send the // 0. rx/ry similar though not the exact same deal because of // their remap. return value != null && value !== 0 && value !== "#000000"; } private addEffectSetting(device: NamedNode, deviceAttr: NamedNode, value: ControlValue): Patch { log(" _addEffectSetting", deviceAttr.value, value); const U = (x: string) => this.graph.Uri(x); const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => this.graph.Quad(s, p, o, this.ctxForEffect); let patch = new Patch([], []); let eset = this.getExistingEset(); if (eset === null) { const ret = this.patchForANewEset(); patch = patch.update(ret.p); eset = ret.eset; } let dsValue; try { dsValue = this.graph.uriValue(eset, U(":value")); } catch (e) { dsValue = this.graph.nextNumberedResource(U(":ds_val")); patch = patch.update(new Patch([], [quad(eset, U(":value"), dsValue)])); } const dset = this.graph.nextNumberedResource(this.uri.value + "_set"); patch = patch.update( new Patch( [], [ quad(dsValue, U(":setting"), dset), quad(dset, U(":device"), device), quad(dset, U(":deviceAttr"), deviceAttr), quad(dset, valuePred(this.graph, deviceAttr), this.nodeForValue(value)), ] ) ); log(" save", patch); this.dsettings.push({ dset, device, deviceAttr, value }); return patch; } private patchExistingDevSetting(devSetting: NamedNode, deviceAttr: NamedNode, value: ControlValue): Patch { log(" patch existing", devSetting.value); return this.graph.getObjectPatch( devSetting, // valuePred(this.graph, deviceAttr), this.nodeForValue(value), this.ctxForEffect ); } private removeEffectSetting(effectSetting: NamedNode): Patch { const U = (x: string) => this.graph.Uri(x); log(" _removeEffectSetting", effectSetting.value); const eset = this.getExistingEset(); if (eset === null) throw "unexpected"; const dsValue = this.graph.uriValue(eset, U(":value")); if (dsValue === null) throw "unexpected"; const toDel = [this.graph.Quad(dsValue, U(":setting"), effectSetting, this.ctxForEffect)]; for (let q of this.graph.subjectStatements(effectSetting)) { toDel.push(q); } return new Patch(toDel, []); } clearAllSettings() { for (let s of this.dsettings) { this.graph.applyAndSendPatch(this.removeEffectSetting(s.dset)); } } private nodeForValue(value: ControlValue): NamedNode | Literal { if (value === null) { throw new Error("no value"); } if (isUri(value)) { return value; } return this.graph.prettyLiteral(value); } }