Files @ 94b58da02abc
Branch filter:

Location: light9/light9/web/live/Effect.ts

drewp@bigasterisk.com
switch to udmx
import debug from "debug";
import { Literal, NamedNode, Quad_Object, Quad_Predicate, Quad_Subject, Term } from "n3";
import { some } from "underscore";
import { Patch, patchContainsPreds, patchUpdate } from "../patch";
import { SyncedGraph } from "../SyncedGraph";

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";
}

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(":scaledValue");
  } else {
    return U(":value");
  }
}

// effect settings data; r/w sync with the graph
export class Effect {
  private settings: Array<{ device: NamedNode; deviceAttr: NamedNode; setting: NamedNode; value: ControlValue }> = [];
  private ctxForEffect: NamedNode
  constructor(
    public graph: SyncedGraph,
    public uri: NamedNode,
    // called if the graph changes our values and not when the caller uses edit()
    private onValuesChanged: (values: void) => void
  ) {
    this.ctxForEffect = this.graph.Uri(this.uri.value.replace("light9.bigasterisk.com/effect", "light9.bigasterisk.com/show/dance2019/effect"));
    graph.runHandler(this.rebuildSettingsFromGraph.bind(this), `effect sync ${uri.value}`);
  }

  addNewEffectToGraph() {
    const U = this.graph.U();
    const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => this.graph.Quad(s, p, o, this.ctxForEffect);

    const addQuads = [
      quad(this.uri, U("rdf:type"), U(":Effect")),
      quad(this.uri, U("rdfs:label"), this.graph.Literal(this.uri.value.replace(/.*\//, ""))),
      quad(this.uri, U(":publishAttr"), U(":strength")),
    ];
    const patch = { adds: addQuads, dels: [] } as Patch;
    log("init new effect", patch);
    this.settings = [];
    this.graph.applyAndSendPatch(patch);
  }

  rebuildSettingsFromGraph(patch?: Patch) {
    const U = this.graph.U();
    if (patch && !patchContainsPreds(patch, [U(":setting"), U(":device"), U(":deviceAttr")])) {
      // that's an approx list of preds , but it just means we'll miss some pathological settings edits
      //   return;
    }

    // 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 seenDevAttrPairs: Set<string> = new Set();

    for (let setting of Array.from(this.graph.objects(this.uri, U(":setting")))) {
      //   log(`  setting ${setting.value}`);
      if (!isUri(setting)) throw new Error();
      let value: ControlValue;
      const device = this.graph.uriValue(setting, U(":device"));
      const deviceAttr = this.graph.uriValue(setting, U(":deviceAttr"));

      const pred = valuePred(this.graph, deviceAttr);
      try {
        value = this.graph.uriValue(setting, pred);
        if (!(value as NamedNode).id.match(/^http/)) {
          throw new Error("not uri");
        }
      } catch (error) {
        try {
          value = this.graph.floatValue(setting, pred);
        } catch (error1) {
          value = this.graph.stringValue(setting, pred); // this may find multi values and throw
        }
      }
      //   log(`change: graph contains ${deviceAttr.value} ${value}`);

      newSettings.push({ device, deviceAttr, setting, value });
    }
    this.settings = newSettings;
    log(`rebuild to ${this.settings.length}`);
    this.onValuesChanged();
  }

  currentValue(device: NamedNode, deviceAttr: NamedNode): ControlValue | null {
    for (let s of this.settings) {
      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 = { adds: [], dels: [] };
    for (let s of this.settings) {
      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.setting}`);
          patchUpdate(result,  this._removeEffectSetting(s.setting));
        }
        existingSetting = s.setting;
      }
    }

    if (newValue !== null && this.shouldBeStored(deviceAttr, newValue)) {
      if (existingSetting === null) {
        patchUpdate(result, this._addEffectSetting(device, deviceAttr, newValue));
      } else {
        patchUpdate(result, this._patchExistingEffectSetting(existingSetting, deviceAttr, newValue));
      }
    } else {
      if (existingSetting !== null) {
        patchUpdate(result, 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";
  }

  _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);
    if (!this.uri) throw new Error("effect unset");
    const setting = this.graph.nextNumberedResource(this.uri.value + "_set");

    const addQuads = [
      quad(this.uri, U(":setting"), setting),
      quad(setting, U(":device"), device),
      quad(setting, U(":deviceAttr"), deviceAttr),
      quad(setting, valuePred(this.graph, deviceAttr), this._nodeForValue(value)),
    ];
    const patch = { adds: addQuads, dels: [] } as Patch;
    log("  save", patch);
    this.settings.push({ device, deviceAttr, setting, value });
    return patch;
  }

  _patchExistingEffectSetting(effectSetting: NamedNode, deviceAttr: NamedNode, value: ControlValue): Patch {
    log("  patch existing", effectSetting.value);
    return this.graph.getObjectPatch(
      effectSetting, //
      valuePred(this.graph, deviceAttr),
      this._nodeForValue(value),
      this.ctxForEffect
    );
  }

  _removeEffectSetting(effectSetting: NamedNode): Patch {
    const U = (x: string) => this.graph.Uri(x);
    log("  _removeEffectSetting", effectSetting.value);
    const toDel = [this.graph.Quad(this.uri, U(":setting"), effectSetting, this.ctxForEffect)];
    for (let q of this.graph.subjectStatements(effectSetting)) {
      toDel.push(q);
    }
    return { dels: toDel, adds: [] };
  }

  _nodeForValue(value: ControlValue): NamedNode | Literal {
    if (value === null) {
      throw new Error("no value");
    }
    if (isUri(value)) {
      return value;
    }
    return this.graph.prettyLiteral(value);
  }
}