Changeset - 9324fc8285ad
[Not reviewed]
default
0 3 0
drewp@bigasterisk.com - 3 years ago 2022-05-31 06:21:50
drewp@bigasterisk.com
Effect repairs duplicate :settings edges when it finds them
3 files changed with 22 insertions and 9 deletions:
0 comments (0 inline, 0 general)
light9/web/live/Effect.ts
Show inline comments
 
import debug from "debug";
 
import { Literal, NamedNode, Quad_Object, Quad_Predicate, Quad_Subject, Term } from "n3";
 
import { some } from "underscore";
 
import { Patch, patchContainsPreds } from "../patch";
 
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 }> = [];
 
@@ -44,113 +44,119 @@ export class Effect {
 
    const U = this.graph.U();
 
    const ctx = this.ctxForEffect();
 
    const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => this.graph.Quad(s, p, o, ctx);
 

	
 
    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) {
 
          throw new Error(`${this.uri.value} had two settings for ${device.value} - ${deviceAttr.value}`);
 
          // 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) {
 
        return this._addEffectSetting(device, deviceAttr, newValue);
 
        patchUpdate(result, this._addEffectSetting(device, deviceAttr, newValue));
 
      } else {
 
        return this._patchExistingEffectSetting(existingSetting, deviceAttr, newValue);
 
        patchUpdate(result, this._patchExistingEffectSetting(existingSetting, deviceAttr, newValue));
 
      }
 
    } else {
 
      if (existingSetting !== null) {
 
        return this._removeEffectSetting(existingSetting);
 
        patchUpdate(result, this._removeEffectSetting(existingSetting));
 
      }
 
    }
 
    return { adds: [], dels: [] };
 
    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 ctx = this.ctxForEffect();
 
    const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => this.graph.Quad(s, p, o, ctx);
 
    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)),
 
    ];
light9/web/live/Light9LiveControls.ts
Show inline comments
 
import debug from "debug";
 
import { css, html, LitElement, PropertyValues } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
import { sortBy, uniq } from "underscore";
 
import { Patch, patchContainsPreds } from "../patch";
 
import { getTopGraph } from "../RdfdbSyncedGraph";
 
import { SyncedGraph } from "../SyncedGraph";
 
import { GraphToControls } from "./GraphToControls";
 
export { EditChoice } from "../EditChoice";
 
export { Light9DeviceControl as Light9LiveDeviceControl } from "./Light9DeviceControl";
 
export { EditChoice } from "../EditChoice";
 
const log = debug("controls");
 

	
 
@customElement("light9-live-controls")
 
export class Light9LiveControls extends LitElement {
 
  graph!: SyncedGraph;
 

	
 
  static styles = [
 
    css`
 
      :host {
 
        display: flex;
 
        flex-direction: column;
 
      }
 
      #preview {
 
        width: 100%;
 
      }
 
      #deviceControls {
 
        flex-grow: 1;
 
        position: relative;
 
        width: 100%;
 
        overflow-y: auto;
 
      }
 

	
 
      light9-live-device-control > div {
 
        break-inside: avoid-column;
light9/web/patch.ts
Show inline comments
 
import * as async from "async";
 
import debug from "debug";
 
import * as async from "async";
 
import { Writer, Parser, Quad, NamedNode } from "n3";
 
import { NamedNode, Parser, Quad, Writer } from "n3";
 
const log = debug("patch");
 

	
 
export interface Patch {
 
  dels: Quad[];
 
  adds: Quad[];
 
  _allPredsCache?: Set<string>;
 
  _allSubjsCache?: Set<string>;
 
}
 

	
 
interface SyncgraphPatchMessage {
 
  patch: { adds: string; deletes: string };
 
}
 

	
 
export function patchUpdate(p1: Patch, p2: Patch): void {
 
  // this is approx, since it doesnt handle matching existing quads.
 
  p1.adds = p1.adds.concat(p2.adds);
 
  p1.dels = p1.dels.concat(p2.dels);
 
}
 

	
 
export function patchSizeSummary(patch: Patch) {
 
  return "-" + patch.dels.length + " +" + patch.adds.length;
 
}
 

	
 
export function parseJsonPatch(input: SyncgraphPatchMessage, cb: (p: Patch) => void) {
 
  // note response cb doesn't have an error arg.
 
  const patch: Patch = { dels: [], adds: [] };
 

	
 
  const parseAdds = (cb: () => any) => {
 
    const parser = new Parser();
 
    return parser.parse(input.patch.adds, (error: any, quad: Quad, prefixes: any) => {
 
      if (quad) {
 
        return patch.adds.push(quad);
 
      } else {
 
        return cb();
 
      }
 
    });
 
  };
 
  const parseDels = (cb: () => any) => {
 
    const parser = new Parser();
 
    return parser.parse(input.patch.deletes, (error: any, quad: any, prefixes: any) => {
 
      if (quad) {
 
        return patch.dels.push(quad);
 
      } else {
 
        return cb();
 
      }
 
    });
 
  };
 

	
 
  // todo: is it faster to run them in series? might be
 
  return async.parallel([parseAdds, parseDels], (err: any) => cb(patch));
 
}
 

	
 
export function toJsonPatch(jsPatch: Patch, cb: { (json: any): any; (arg0: any): any }) {
 
  const out: SyncgraphPatchMessage = { patch: { adds: "", deletes: "" } };
 

	
 
  const writeDels = function (cb: () => any) {
 
    const writer = new Writer({ format: "N-Quads" });
 
    writer.addQuads(jsPatch.dels);
 
    return writer.end(function (err: any, result: string) {
 
      out.patch.deletes = result;
 
      return cb();
 
    });
 
  };
 

	
 
  const writeAdds = function (cb: () => any) {
 
    const writer = new Writer({ format: "N-Quads" });
 
    writer.addQuads(jsPatch.adds);
 
    return writer.end(function (err: any, result: string) {
 
      out.patch.adds = result;
 
      return cb();
 
    });
 
  };
 

	
0 comments (0 inline, 0 general)