diff --git a/light9/web/graph.coffee b/light9/web/graph.coffee new file mode 100644 --- /dev/null +++ b/light9/web/graph.coffee @@ -0,0 +1,148 @@ +# Patch is {addQuads: , delQuads: } +# is [{subject: s, ...}, ...] + +# partial port of autodepgraphapi.py +class GraphWatchers + constructor: -> + @handlersSp = {} # {s: {p: [handlers]}} + subscribe: (s, p, o, onChange) -> # return subscription handle + if o? then throw Error('not implemented') + if not @handlersSp[s] + @handlersSp[s] = {} + if not @handlersSp[s][p] + @handlersSp[s][p] = [] + @handlersSp[s][p].push(onChange) + + unsubscribe: (subscription) -> + throw Error('not implemented') + + graphChanged: (patch) -> + for quad in patch.delQuads + for cb in ((@handlersSp[quad.subject] || {})[quad.predicate] || []) + # currently calls multiple times, which is ok, but we might + # group things into fewer patches + cb({delQuads: [quad], addQuads: []}) + for quad in patch.addQuads + for cb in ((@handlersSp[quad.subject] || {})[quad.predicate] || []) + cb({delQuads: [], addQuads: [quad]}) + + +class window.SyncedGraph + # Note that applyPatch is the only method to write to the graph, so + # it can fire subscriptions. + + constructor: (patchSenderUrl, prefixes) -> + @graph = N3.Store() + @_addPrefixes(prefixes) + @_watchers = new GraphWatchers() + + _addPrefixes: (prefixes) -> + @graph.addPrefixes(prefixes) + + Uri: (curie) -> + N3.Util.expandPrefixedName(curie, @graph._prefixes) + + Literal: (jsValue) -> + N3.Util.createLiteral(jsValue) + + toJs: (literal) -> + # incomplete + parseFloat(N3.Util.getLiteralValue(literal)) + + loadTrig: (trig, cb) -> # for debugging + patch = {delQuads: [], addQuads: []} + parser = N3.Parser() + parser.parse trig, (error, quad, prefixes) => + if (quad) + patch.addQuads.push(quad) + else + @applyPatch(patch) + @_addPrefixes(prefixes) + cb() if cb + + quads: () -> # for debugging + [q.subject, q.predicate, q.object, q.graph] for q in @graph.find() + + applyAndSendPatch: (patch, cb) -> + @applyPatch(patch) + console.log('patch to server:', patch) + # post to server + + applyPatch: (patch) -> + # In most cases you want applyAndSendPatch. + # + # This is the only method that writes to the graph! + for quad in patch.delQuads + @graph.removeTriple(quad) + for quad in patch.addQuads + @graph.addTriple(quad) + @_watchers.graphChanged(patch) + + getObjectPatch: (s, p, newObject, g) -> + # send a patch which removes existing values for (s,p,*,c) and + # adds (s,p,newObject,c). Values in other graphs are not affected. + existing = @graph.findByIRI(s, p, null, g) + return { + delQuads: existing, + addQuads: [{subject: s, predicate: p, object: newObject, graph: g}] + } + + patchObject: (s, p, newObject, g) -> + @applyAndSendPatch(@getObjectPatch(s, p, newObject, g)) + + + subscribe: (s, p, o, onChange) -> # return subscription handle + # onChange is called with a patch that's limited to the quads + # that match your request. + # We call you immediately on existing triples. + @_watchers.subscribe(s, p, o, onChange) + immediatePatch = {delQuads: [], addQuads: @graph.findByIRI(s, p, o)} + if immediatePatch.addQuads.length + onChange(immediatePatch) + + unsubscribe: (subscription) -> + @_watchers.unsubscribe(subscription) + + floatValue: (s, p) -> + quads = @graph.findByIRI(s, p) + switch quads.length + when 0 then throw new Error("no value for "+s+" "+p) + when 1 + obj = quads[0].object + return parseFloat(N3.Util.getLiteralValue(obj)) + else + throw new Error("too many values: " + JSON.stringify(quads)) + + stringValue: (s, p) -> + + uriValue: (s, p) -> + + objects: (s, p) -> + + subjects: (p, o) -> + + items: (list) -> + + contains: (s, p, o) -> + +### +rdfstore.create((err, store) -> + window.store = store + store.setPrefix('l9', "http://light9.bigasterisk.com/") + store.setPrefix('xsd', "http://www.w3.org/2001/XMLSchema#") + store.load('text/turtle', " +@prefix : . +@prefix dev: . + +:demoResource :startTime 0.5 . + ", (err, n) -> + console.log('loaded', n) + store.graph (err, graph) -> + window.graph = graph + + ) + window.URI = (curie) -> store.rdf.createNamedNode(store.rdf.resolve(curie)) + window.Lit = (value, dtype) -> store.rdf.createLiteral(value, null, URI(dtype)) + + ) +### \ No newline at end of file diff --git a/light9/web/timeline-elements.html b/light9/web/timeline-elements.html --- a/light9/web/timeline-elements.html +++ b/light9/web/timeline-elements.html @@ -2,7 +2,6 @@ - @@ -25,17 +24,13 @@ } light9-timeline-diagram-layer, light9-timeline-adjusters { position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; + left: 0; top: 0; right: 0; bottom: 0; }
timeline editor: song [uri]
- - + - + @@ -146,167 +111,48 @@ @@ -523,25 +373,18 @@ border: 3px yellow dotted; border-radius: 8px; padding: 5px; + + cursor: ew-resize; + -webkit-user-select: none; } - +
- +
{{value}}
{{value}}
-
@@ -557,3 +400,9 @@ }); + + + + + + diff --git a/light9/web/timeline.coffee b/light9/web/timeline.coffee new file mode 100644 --- /dev/null +++ b/light9/web/timeline.coffee @@ -0,0 +1,124 @@ + +window.graph = new SyncedGraph('noServerYet', { +'': 'http://light9.bigasterisk.com/', +'xsd', 'http://www.w3.org/2001/XMLSchema#', + }) + +window.graph.loadTrig(" +@prefix : . +@prefix dev: . + + { + :demoResource :startTime 0.5 . +} + ") + + +class Adjustable + # + constructor: (@config) -> + @center = @config.getTarget().add(@config.getSuggestedTargetOffset()) + @getValue = @config.getValue + + +Polymer + is: 'light9-timeline-editor' + behaviors: [ Polymer.IronResizableBehavior ] + properties: + viewState: { type: Object } + ready: -> + @viewState = + zoomSpec: + duration: 190 + t1: 102 + t2: 161 + cursor: + t: 105 + + @fullZoomX = d3.scale.linear().domain([0, @viewState.zoomSpec.duration]).range([0, @offsetWidth]) + + animCursor = () => + @viewState.cursor.t = 130 + 20 * Math.sin(Date.now() / 2000) + @$.dia.setCursor(@$.audio.offsetTop, @$.audio.offsetHeight, + @$.zoomed.$.time.offsetTop, @$.zoomed.$.time.offsetHeight, + @viewState.zoomSpec, @viewState.cursor) + + @set('viewState.zoomSpec.t1', 80 + 10 * Math.sin(Date.now() / 3000)) + + setInterval(animCursor, 50) + + setTimeout(() => + @adjs = @persistDemo() #@makeZoomAdjs().concat(@persistDemo()) + , 3000) + + + persistDemo: -> + adj = new Adjustable({ + getValue: () => (graph.floatValue( + graph.Uri(':demoResource'), + graph.Uri(':startTime'))) + getTarget: () => $V([200, 300]) + getSuggestedTargetOffset: () => $V([-30, 0]) + }) + console.log(adj.config.getValue()) + + return [adj] + + makeZoomAdjs: -> + left = new Adjustable({ + getValue: () => (@viewState.zoomSpec.t1) + getTarget: () => $V([@fullZoomX(@viewState.zoomSpec.t1), @$.audio.offsetTop + @$.audio.offsetHeight / 2]) + getSuggestedTargetOffset: () => $V([-30, 0]) + }) + + right = new Adjustable({ + getValue: () => (@viewState.zoomSpec.t2) + getTarget: () => $V([@fullZoomX(@viewState.zoomSpec.t2), @$.audio.offsetTop + @$.audio.offsetHeight / 2]) + getSuggestedTargetOffset: () => $V([30, 0]) + }) + return [left, right] + + +Polymer + is: 'light9-timeline-adjuster' + properties: + adj: + type: Object + notify: true + target: + type: Object + notify: true + value: + type: String + computed: '_value(adj.value)' + centerStyle: + computed: '_centerStyle(adj.center)' + + _value: (adjValue) -> + d3.format(".4g")(adjValue) + + _centerStyle: (center) -> + { + x: center.e(1) + y: center.e(2) + } + + ready: -> + subj = graph.Uri(':demoResource') + pred = graph.Uri(':startTime') + ctx = graph.Uri('http://example.com/') + graph.subscribe subj, pred, null, (patch) => + for q in patch.addQuads + @set('adj.value', graph.toJs(q.object)) + + drag = d3.behavior.drag() + sel = d3.select(@$.label) + sel.call(drag) + drag.origin((d) -> {x: @offsetLeft, y: @offsetTop}) + drag.on 'dragstart', () => + drag._startValue = @adj.getValue() + + drag.on 'drag', () => + console.log('drag', d3.event) + newValue = drag._startValue + d3.event.x * .1 + graph.patchObject(subj, pred, graph.Literal(newValue), ctx) diff --git a/show/dance2016/timelinedemo.n3 b/show/dance2016/timelinedemo.n3 new file mode 100644