From d2300e7d4f7bccbcc648a4aa30aa1f60c611d372 Mon Sep 17 00:00:00 2001 From: "David M. Weigl" Date: Thu, 9 Aug 2018 13:37:57 +0100 Subject: [PATCH 01/10] stub for generalised traversal --- src/actions/index.js | 71 ++++++++++++++++++++++++++++++++++++++++++ src/containers/test.js | 19 +++++++++-- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index 6acc22a..279511c 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -52,6 +52,7 @@ export const TICK="TICK"; export const muzicodesUri = "http://127.0.0.1:5000/MUZICODES" export const MAX_RETRIES = 3; +export const MAX_TRAVERSAL_HOPS = 10; export const RETRY_DELAY = 10; // TODO move context somewhere global -- most framing happens server side @@ -108,6 +109,76 @@ export function fetchTEI(uri) { } } +export function traverse( + subjectUri, + objectPrefixWhitelist=[], objectUriWhitelist=[], + objectPrefixBlacklist=[], objectUriBlacklist=[], + propertyPrefixWhitelist=[], propertyUriWhitelist=[], + propertyPrefixBlacklist=[], propertyUriBlacklist=[], + goals={}, numHops=MAX_TRAVERSAL_HOPS, + useEtag = false, etag="") { + // PURPOSE: + // ************************************************************************* + // Traverse through a graph, looking for entities of interest + // (keys of 'goals') and undertaking actions in response + // (values of 'goals'". + // For each subject, traverse along its predicates to its attached objects, + // then recurse (each object becomes subject in next round). + // When recursing, check for instances of object-as-subject in the current + // file (internal traversal), AND do an HTTP GET to resolve the object URI + // and recurse there (external traversal). + // If useEtag is specified, then worry about etags for external traversals + // (and re-request if the file has changed) + // n.b. this is only an issue for dynamic MELD deployments + // With each hop, decrement numHops. + // Stop when numHops reaches zero, or when there are no more objects + // to traverse to. + // If an objectPrefixWhitelist is specified, only traverse to objects with + // URIs that start with a prefix in the list. + // If an objectUriWhitelist is specified, only traverse to objects with + // URIs in the list. + // If an objectPrefixBlacklist is specified, only traverse to objects with + // URIs that do NOT start with a prefix in the list. + // If an objectUriBlacklist is specified, only traverse to objects with + // URIs that are NOT in the list. + // If a propertyPrefixWhitelist is specified, only traverse to objects along + // properties whose URIs start with a prefix in the list. + // If a propertyUriWhitelist is specified, only traverse to objects along + // properties with URIs in the list. + // If a propertyPrefixBlacklist is specified, only traverse to objects along + // properties whose URIs do NOT start with a prefix in the list. + // If a propertyUriWhitelist is specified, only traverse to objects along + // properties with URIs that are NOT in the list. + // ************************************************************************* + + // set up HTTP request + const headers = {'Accept': 'application/ld+json'}; + if(useEtag) { + headers['If-None-Match'] = etag; + } + const promise = axios.get(subjectUri, { + headers: headers, + validateStatus: function(stat) { + // only complain if code is greater or equal to 400 + // this is to not treat 304's as errors} + return stat < 400; + } + }); + return (dispatch) => { + promise.then( (response) => { + if(response.status == 304) { + return; // file not modified, i.e. etag matched, no updates required + } + console.log("RESPONSE: ", response) + let subject = response.data; + if("@graph" in subject) { + subject = subject["@graph"] // json-ld payload + } + console.log("Traversal hit subject: ", subject) + }); + } +} + export function fetchSessionGraph(uri, etag = "") { // console.log("FETCH_SESSION_GRAPH ACTION ON URI: ", uri, " with etag: ", etag); // TODO add etag to header as If-None-Match and enable corresponding support on server diff --git a/src/containers/test.js b/src/containers/test.js index 9cec8b3..26c155b 100644 --- a/src/containers/test.js +++ b/src/containers/test.js @@ -1,11 +1,24 @@ import React, { Component } from 'react'; -import IIIF from "../components/iiif"; +import { traverse } from '../actions/index'; +import { connect } from 'react-redux' ; +import { bindActionCreators } from 'redux'; -export default class Test extends Component { +class Test extends Component { constructor(props) { super(props); } + + componentDidMount() { + this.props.traverse("http://meld.linkedmusic.org/annotations/meld-test.json-ld"); + } + render() { - return + return
Hello MELD
} } + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ traverse }, dispatch); +} + +export default connect(null,mapDispatchToProps)(Test); From 5e8c775506d9eac325ee6905b79b17f2a29f18f3 Mon Sep 17 00:00:00 2001 From: "David M. Weigl" Date: Tue, 21 Aug 2018 16:56:28 +0100 Subject: [PATCH 02/10] Initial implementation of generalised traversal. Encountered documents are stored in a in-session graph via graph reducer. app-specified objectives are matched against the graph as it accumulates, and outcomes are stored in a corresponding array. App-specific components then need to make appropriate use of this. WIP - still need to convert RDF to JSON-LD transparently. --- package.json | 2 +- src/actions/index.js | 163 +++++++++++++++++++++++++++++----- src/containers/test.js | 39 +++++++- src/reducers/reducer_graph.js | 63 ++++++++++--- 4 files changed, 229 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index ea1b1ff..74ad197 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "babel-preset-stage-1": "^6.1.18", "immutability-helper": "^2.1.2", "jsonld": "^0.4.11", - "leaflet": "^1.2.0", + "leaflet": "^1.2.0", "lodash": "^3.10.1", "n3": "^0.10.0", "querystring": "^0.2.0", diff --git a/src/actions/index.js b/src/actions/index.js index 279511c..7bf348b 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -3,12 +3,15 @@ import jsonld from 'jsonld' import querystring from 'querystring'; import { ANNOTATION_PATCHED, ANNOTATION_POSTED, ANNOTATION_HANDLED, ANNOTATION_NOT_HANDLED, ANNOTATION_SKIPPED } from './meldActions'; -export const HAS_BODY = "oa:hasBody" +export const SET_TRAVERSAL_OBJECTIVES = "SET_TRAVERSAL_OBJECTIVES"; +export const APPLY_TRAVERSAL_OBJECTIVE = "APPLY_OBJECTIVE"; +export const HAS_BODY = "oa:hasBody"; export const FETCH_SCORE = 'FETCH_SCORE'; export const FETCH_RIBBON_CONTENT = 'FETCH_RIBBON_CONTENT'; export const FETCH_CONCEPTUAL_SCORE = 'FETCH_CONCEPTUAL_SCORE'; export const FETCH_TEI = 'FETCH_TEI'; export const FETCH_GRAPH = 'FETCH_GRAPH'; +export const FETCH_GRAPH_DOCUMENT = 'FETCH_GRAPH_DOCUMENT'; export const FETCH_WORK = 'FETCH_WORK'; export const FETCH_TARGET_EXPRESSION = 'FETCH_TARGET_EXPRESSION'; export const FETCH_COMPONENT_TARGET = 'FETCH_COMPONENT_TARGET'; @@ -110,22 +113,24 @@ export function fetchTEI(uri) { } export function traverse( - subjectUri, - objectPrefixWhitelist=[], objectUriWhitelist=[], - objectPrefixBlacklist=[], objectUriBlacklist=[], - propertyPrefixWhitelist=[], propertyUriWhitelist=[], - propertyPrefixBlacklist=[], propertyUriBlacklist=[], - goals={}, numHops=MAX_TRAVERSAL_HOPS, - useEtag = false, etag="") { + docUri, + params = { + objectPrefixWhitelist:[], objectUriWhitelist:[], objectTypeWhitelist : [], + objectPrefixBlacklist:[], objectUriBlacklist:[], objectTypeBlacklist : [], + propertyPrefixWhitelist:[], propertyUriWhitelist:[], + propertyPrefixBlacklist:[], propertyUriBlacklist:[], + objectives:{}, numHops:MAX_TRAVERSAL_HOPS, + useEtag : false, etag:"" + }) { // PURPOSE: // ************************************************************************* // Traverse through a graph, looking for entities of interest - // (keys of 'goals') and undertaking actions in response - // (values of 'goals'". + // (keys of 'objectives') and undertaking actions in response + // (values of 'objectives'). // For each subject, traverse along its predicates to its attached objects, // then recurse (each object becomes subject in next round). // When recursing, check for instances of object-as-subject in the current - // file (internal traversal), AND do an HTTP GET to resolve the object URI + // file (traverseInternal), AND do an HTTP GET to resolve the object URI // and recurse there (external traversal). // If useEtag is specified, then worry about etags for external traversals // (and re-request if the file has changed) @@ -137,10 +142,14 @@ export function traverse( // URIs that start with a prefix in the list. // If an objectUriWhitelist is specified, only traverse to objects with // URIs in the list. + // If an objectTypeWhitelist is specified, only traverse to objects with + // a type that's in the list. // If an objectPrefixBlacklist is specified, only traverse to objects with // URIs that do NOT start with a prefix in the list. // If an objectUriBlacklist is specified, only traverse to objects with // URIs that are NOT in the list. + // If an objectTypeBlacklist is specified, do NOT traverse to objects with + // types in the list. // If a propertyPrefixWhitelist is specified, only traverse to objects along // properties whose URIs start with a prefix in the list. // If a propertyUriWhitelist is specified, only traverse to objects along @@ -153,10 +162,12 @@ export function traverse( // set up HTTP request const headers = {'Accept': 'application/ld+json'}; - if(useEtag) { - headers['If-None-Match'] = etag; + if(params["useEtag"]) { + headers['If-None-Match'] = params["etag"]; } - const promise = axios.get(subjectUri, { + + console.log("FETCHING: ", docUri, params); + const promise = axios.get(docUri, { headers: headers, validateStatus: function(stat) { // only complain if code is greater or equal to 400 @@ -169,16 +180,117 @@ export function traverse( if(response.status == 304) { return; // file not modified, i.e. etag matched, no updates required } - console.log("RESPONSE: ", response) - let subject = response.data; - if("@graph" in subject) { - subject = subject["@graph"] // json-ld payload + console.log(response.headers["content-type"]); + let data = response.data; + // appropriately handle content types +// if(isRDF(response.headers["content-type"])) { +// toNQuads( +// +// } +// switch(response.headers["content-type"]) { +// // If we are working with RDF, we need to convert it to JSON-LD. +// // Unfortunately jsonld.js only reads nquads. +// // Thus, convert non-nquad RDF formats to nquad first +// case "text/turtle": +// case "application/trig": +// case "application/n-triples": +// case "text/n3": +// + + if(response.headers["content-type"] === "application/ld+json") { + // expand the JSON-LD object so that we are working with full URIs, not compacted into prefixes + jsonld.expand(response.data, (err, expanded) => { + if(err) { console.log("EXPANSION ERROR: ", docUri, err); } + // flatten the expanded JSON-LD object so that each described entity has an ID at the top-level of the tree + jsonld.flatten(expanded, (err, flattened) => { + dispatch({ + type: FETCH_GRAPH_DOCUMENT, + payload: flattened + }); + // convert the flattened array of JSON-LD structures into a lookup table using each entity's URI ("@id") + let idLookup = {} + Object.entries(flattened).forEach( ([key, value]) => { + idLookup[value["@id"]] = value; + }) + Object.entries(idLookup).forEach( ([subjectUri, subjectDescription]) => { + // iterating through each entity within the document as the subject, + // look at its description (set of predicate-object tuples). + Object.entries(subjectDescription).forEach( ([pred,objs]) => { + // because JSON-LD, objs could be a single object or an array of objects + // therefore, ensure consistency: + objs = Array.isArray(objs) ? objs : [objs] + objs.map( (obj) => { + if(obj === Object(obj)) { + // our *RDF* object is a *JAVASCRIPT* object + // but because we've flattened our document, we know that it will contain only an @id + // and that all of its other descriptors will be associated with that @id at the top-level + // (which we will handle in another iteration) + // CHECK FOR OBJECTIVES HERE + console.log("<>", subjectUri, pred, obj["@id"], docUri); + // Now recurse (if black/whitelist conditions and hop counter allow) + // Remember that we've already visited the current document to avoid loops + if( params["numHops"] !== 0 && !(params["objectUriBlacklist"].includes(obj["@id"])) ) { + dispatch(traverse(obj["@id"], { + ...params, + "objectUriBlacklist": params["objectUriBlacklist"].concat(docUri), + "numHops": params["numHops"]-1 + })) + } + } else { + // our *RDF* object is a literal + // n.b. exceptions where pred is @type, @id, etc. There, the obj is still a URI, not a literal + // Could test for those explicitly here. + // CHECK FOR OBJECTIVES HERE + console.log("||", subjectUri, pred, obj, docUri) + + } + }); + }) + + }) + }); + // since we will sometimes obtain arrays, do the following to ensure consistency: + // expanded = Array.isArray(expanded) ? expanded : [ expanded ]; + // expanded.map( (subject) => { + // // we've encountered a subject of interest. + // // traverse through its associated predicates and objects: + // const predicates = Object.keys(subject).filter((p) => { if(!(p in propertyUriBlacklist)) return p }); + // predicates.map( (p) => { + // console.log(p); + // }); + // }); + }) } - console.log("Traversal hit subject: ", subject) - }); + }).catch( (err) => console.log("Could not retrieve ", docUri, err)); } } +export function checkTraversalObjectives(graph, objectives) { + // check a given json-ld structure against a set of objectives (json-ld frames) + return (dispatch) => { + objectives.map( (obj, ix) => { + jsonld.frame(graph, obj, (err, framed) => { + if(err) { + console.log("FRAMING ERROR: ", objectives[ix], err); + } else { + dispatch({ + type:APPLY_TRAVERSAL_OBJECTIVE, + payload: {ix, framed} + }) + } + }) + }) + } +} + +export function setTraversalObjectives(objectives) { + return { + type: SET_TRAVERSAL_OBJECTIVES, + payload:objectives + } +} + + export function fetchSessionGraph(uri, etag = "") { // console.log("FETCH_SESSION_GRAPH ACTION ON URI: ", uri, " with etag: ", etag); // TODO add etag to header as If-None-Match and enable corresponding support on server @@ -907,6 +1019,17 @@ export function ensureArray(theObj, theKey) { } } + +// Function to set up the objectives (objects containing JSON-LD frames) +// matched against the graph being built during a traversal. +// Typically called once, on componentWillMount +export function configureTraversalObjectives(objectives) { + return { + type: SET_TRAVERSAL_OBJECTIVES, + payload: objectives + } +} + export function createSession(sessionsUri, scoreUri, {session="", etag="", retries=MAX_RETRIES, performerUri="", slug=""} = {}) { return (dispatch) => { if(retries) { diff --git a/src/containers/test.js b/src/containers/test.js index 26c155b..a82e62f 100644 --- a/src/containers/test.js +++ b/src/containers/test.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { traverse } from '../actions/index'; +import { traverse, setTraversalObjectives, checkTraversalObjectives } from '../actions/index'; import { connect } from 'react-redux' ; import { bindActionCreators } from 'redux'; @@ -8,8 +8,35 @@ class Test extends Component { super(props); } + componentWillMount() { + this.props.setTraversalObjectives([ + { + "@context": { + "oa": "http://www.w3.org/ns/oa#", + "meldterm": "http://meld.linkedmusic.org/terms/" + }, + "@id": {}, + "oa:hasBody": { + "@id": "meldterm:highlight", + + } + } + ]); + } + componentDidMount() { - this.props.traverse("http://meld.linkedmusic.org/annotations/meld-test.json-ld"); + // start traversal + this.props.traverse("http://meld.linkedmusic.org/annotations/Frageverbot1.json-ld"); + } + + componentDidUpdate(prevProps, prevState) { + console.log("Did update!", prevProps, this.props); + if("graph" in prevProps) { + // check our traversal objectives if the graph has updated + if(prevProps.graph.graph.length !== this.props.graph.graph.length) { + this.props.checkTraversalObjectives(this.props.graph.graph, this.props.graph.objectives); + } + } } render() { @@ -17,8 +44,12 @@ class Test extends Component { } } +function mapStateToProps({ graph }) { + return { graph }; +} + function mapDispatchToProps(dispatch) { - return bindActionCreators({ traverse }, dispatch); + return bindActionCreators({ traverse, setTraversalObjectives, checkTraversalObjectives }, dispatch); } -export default connect(null,mapDispatchToProps)(Test); +export default connect(mapStateToProps,mapDispatchToProps)(Test); diff --git a/src/reducers/reducer_graph.js b/src/reducers/reducer_graph.js index c365d42..fc4c941 100644 --- a/src/reducers/reducer_graph.js +++ b/src/reducers/reducer_graph.js @@ -1,25 +1,32 @@ import update from 'immutability-helper'; +import jsonld from 'jsonld' import { FETCH_GRAPH, FETCH_WORK, FETCH_COMPONENT_TARGET, SESSION_GRAPH_ETAG, + FETCH_GRAPH_DOCUMENT, + SET_TRAVERSAL_OBJECTIVES, + APPLY_TRAVERSAL_OBJECTIVE, ensureArray } from '../actions/index' import { QUEUE_NEXT_SESSION } from '../actions/meldActions'; const INIT_STATE = { - graph: { - annoGraph: {}, - targetsById: {}, - targetsByType: {} - }, +// graph: { +// annoGraph: {}, +// targetsById: {}, +// targetsByType: {} +// }, etags: {}, nextSession: "", - info: {} + info: {}, + graph: [], + objectives: [], + outcomes: [] } -export default function(state = INIT_STATE, action) { +export default function (state = INIT_STATE, action) { switch (action.type) { /* case FETCH_GRAPH: @@ -123,11 +130,41 @@ export default function(state = INIT_STATE, action) { return update(state, { nextSession: { $set: action.payload } }); - case FETCH_WORK: - if(action.payload.info){ - return update(state, { info: {$merge: { [action.payload.target["@id"]]: action.payload.info } }}); - }; - default: - return state; + case FETCH_WORK: + if(action.payload.info){ + return update(state, { info: {$merge: { [action.payload.target["@id"]]: action.payload.info } }}); + }; + case SET_TRAVERSAL_OBJECTIVES: + // register the set of objectives provided by the MELD application + // and initialise the outcomes in a corresponding array. + // Typically run once on mount. + return update(state, { + objectives: { + $set: action.payload + }, + outcomes: { + $set: new Array(action.payload.length) + } + }); + case FETCH_GRAPH_DOCUMENT: + // new graph fragment has arrived. Add it to our graph and check our objectives. + return update(state, { + graph: { + $push: action.payload + } + }); + case APPLY_TRAVERSAL_OBJECTIVE: + // an objective has been applied against the graph. Store the outcome at the + // appropriate index. + let updatedOutcomes = state.outcomes; + updatedOutcomes[action.payload.ix] = action.payload.framed; + return update(state, { + outcomes: { + $set: updatedOutcomes + } + }); + default: + return state; } } + From c8f68632255612b86ac033bd114934da83967e66 Mon Sep 17 00:00:00 2001 From: "David M. Weigl" Date: Wed, 22 Aug 2018 15:17:55 +0100 Subject: [PATCH 03/10] testframe bugfix --- src/containers/test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/containers/test.js b/src/containers/test.js index a82e62f..b2593f9 100644 --- a/src/containers/test.js +++ b/src/containers/test.js @@ -17,8 +17,7 @@ class Test extends Component { }, "@id": {}, "oa:hasBody": { - "@id": "meldterm:highlight", - + "@id": "meldterm:highlight" } } ]); From 81d0de38ad5b626b8a5dd75419d2ca98b8e0427f Mon Sep 17 00:00:00 2001 From: "David M. Weigl" Date: Fri, 24 Aug 2018 12:41:57 +0100 Subject: [PATCH 04/10] add outcomes hash so applications know that outcomes have changed --- package.json | 1 + src/reducers/reducer_graph.js | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 74ad197..7c1c0ff 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "babel-preset-stage-1": "^6.1.18", "immutability-helper": "^2.1.2", "jsonld": "^0.4.11", + "jsum": "^0.1.4", "leaflet": "^1.2.0", "lodash": "^3.10.1", "n3": "^0.10.0", diff --git a/src/reducers/reducer_graph.js b/src/reducers/reducer_graph.js index fc4c941..5cf598b 100644 --- a/src/reducers/reducer_graph.js +++ b/src/reducers/reducer_graph.js @@ -1,5 +1,6 @@ import update from 'immutability-helper'; import jsonld from 'jsonld' +import JSum from 'jsum' import { FETCH_GRAPH, FETCH_WORK, @@ -23,7 +24,8 @@ const INIT_STATE = { info: {}, graph: [], objectives: [], - outcomes: [] + outcomes: [], + outcomesHash: "" } export default function (state = INIT_STATE, action) { @@ -158,9 +160,13 @@ export default function (state = INIT_STATE, action) { // appropriate index. let updatedOutcomes = state.outcomes; updatedOutcomes[action.payload.ix] = action.payload.framed; + let updatedOutcomesHash = JSum.digest(updatedOutcomes, 'md5', 'hex') return update(state, { outcomes: { $set: updatedOutcomes + }, + outcomesHash: { + $set: updatedOutcomesHash } }); default: From 83c82a16f3db45e2de29ab7e997ec923ab9759ae Mon Sep 17 00:00:00 2001 From: "David M. Weigl" Date: Fri, 24 Aug 2018 13:00:40 +0100 Subject: [PATCH 05/10] import timesync changes from master --- src/reducers/index.js | 4 ++- src/reducers/reducer_timesync.js | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/reducers/reducer_timesync.js diff --git a/src/reducers/index.js b/src/reducers/index.js index d28e351..170147a 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -5,6 +5,7 @@ import TEIReducer from './reducer_tei'; import AppReducer from './reducer_app'; import SessionControlReducer from './reducer_sessionControl' import ModalUIReducer from './reducer_modalUI' +import TimeSyncReducer from './reducer_timesync' var reducerSets = { graph: GraphReducer, @@ -12,7 +13,8 @@ var reducerSets = { tei: TEIReducer, app: AppReducer, sessionControl: SessionControlReducer, - modalUI: ModalUIReducer + modalUI: ModalUIReducer, + timesync: TimeSyncReducer }; export var reducers = combineReducers(reducerSets); diff --git a/src/reducers/reducer_timesync.js b/src/reducers/reducer_timesync.js new file mode 100644 index 0000000..59fc9f6 --- /dev/null +++ b/src/reducers/reducer_timesync.js @@ -0,0 +1,55 @@ +import update from 'immutability-helper'; +import { parse } from 'querystring'; +import { FETCH_GRAPH, PROCESS_ANNOTATION, TICK } from '../actions/index'; + +export default function(state = { mediaResources: {} }, action) { + let mediaResourcesToAdd = {}; + switch(action.type) { + case FETCH_GRAPH: + // parse through graph looking for timed media resources + // to add to our state for potential timed annotation tracking + // n.b. will need fixing if we change our manifest structures + action.payload["@graph"][0]["ldp:contains"].map( (anno) => { + anno["oa:hasTarget"].map( (target) => { + if((target["@type"] === "meldterm:AudioManifestation" || + target["@type"] === "meldterm:VideoManifestation") + && + !(target["@id"] in state["mediaResources"])) { + mediaResourcesToAdd[target["@id"]] = {times:{}, currentTime:0}; + } + }); + }); + return update(state, { $merge: { mediaResources: mediaResourcesToAdd } }); + case PROCESS_ANNOTATION: + mediaResourcesToAdd = state["mediaResources"] + action.payload.targets.map( (t) => { + // only interested if a) we have a timed media fragment and + // b) we know about the media resource that this is a fragment of + const targetUriComponents = t["@id"].split('#') + const baseResource = targetUriComponents[0] + if(targetUriComponents.length > 1){ + const params = parse(targetUriComponents[1]) + if("t" in params) { + // have a timed media fragment + if(baseResource in mediaResourcesToAdd) { + // we know about this media resource + // keep track of the annotation at this time + mediaResourcesToAdd[baseResource]["times"][params["t"]] = action.payload.bodies + } + } + } + }) + return update(state, { $set: { "mediaResources": mediaResourcesToAdd } }) + case TICK: + return update(state, { + "mediaResources": { + [action.payload.uri]: { + $merge: { + "currentTime": action.payload.time, + } + } + } + }) + default: return state; + } +} From b7934428b44bde81a07b87153286a3f0c37e014c Mon Sep 17 00:00:00 2001 From: "David M. Weigl" Date: Fri, 24 Aug 2018 13:15:07 +0100 Subject: [PATCH 06/10] add generalised traversal support to the timesync reducer --- src/reducers/reducer_timesync.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/reducers/reducer_timesync.js b/src/reducers/reducer_timesync.js index 59fc9f6..aa008a7 100644 --- a/src/reducers/reducer_timesync.js +++ b/src/reducers/reducer_timesync.js @@ -1,6 +1,7 @@ import update from 'immutability-helper'; import { parse } from 'querystring'; import { FETCH_GRAPH, PROCESS_ANNOTATION, TICK } from '../actions/index'; +const REGISTER_CLOCK = "REGISTER_CLOCK"; export default function(state = { mediaResources: {} }, action) { let mediaResourcesToAdd = {}; @@ -20,6 +21,13 @@ export default function(state = { mediaResources: {} }, action) { }); }); return update(state, { $merge: { mediaResources: mediaResourcesToAdd } }); + case REGISTER_CLOCK: + // alternative, more flexible means to accomplish the result of the FETCH_GRAPH + // action above (for use with generalised traversal) + if(!(action.payload in state["mediaResources"])) { + mediaResourcesToAdd[action.payload] = {times:{}, currentTime:0}; + } + return update(state, { $merge: { mediaResources: mediaResourcesToAdd } }); case PROCESS_ANNOTATION: mediaResourcesToAdd = state["mediaResources"] action.payload.targets.map( (t) => { From 9aa238204c7065e69bd16ebea4246f45fa1a6775 Mon Sep 17 00:00:00 2001 From: "David M. Weigl" Date: Fri, 24 Aug 2018 13:21:02 +0100 Subject: [PATCH 07/10] add register clock function to support timesync with generalised traversal --- src/actions/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/actions/index.js b/src/actions/index.js index 7bf348b..c0ad0af 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -1108,3 +1108,11 @@ export function tickTimedResource(resourceUri, time) { } } } + +export function registerClock(clockUri) { + return { + type: "REGISTER_CLOCK", + payload: clockUri + } +} + From 912372dcd2ba4350902679a30e5ae03a1b7a58a2 Mon Sep 17 00:00:00 2001 From: "David M. Weigl" Date: Tue, 28 Aug 2018 16:30:51 +0100 Subject: [PATCH 08/10] enable single-item targtes in timesync reducer! --- src/reducers/reducer_timesync.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/reducers/reducer_timesync.js b/src/reducers/reducer_timesync.js index aa008a7..05d3043 100644 --- a/src/reducers/reducer_timesync.js +++ b/src/reducers/reducer_timesync.js @@ -30,6 +30,10 @@ export default function(state = { mediaResources: {} }, action) { return update(state, { $merge: { mediaResources: mediaResourcesToAdd } }); case PROCESS_ANNOTATION: mediaResourcesToAdd = state["mediaResources"] + // ensure targets are an array + if(!(Array.isArray(action.payload.targets))) { + action.payload.targets = [action.payload.targets] + } action.payload.targets.map( (t) => { // only interested if a) we have a timed media fragment and // b) we know about the media resource that this is a fragment of From 98e29e878d51a0db6c53278f693769d61a9afca6 Mon Sep 17 00:00:00 2001 From: "David M. Weigl" Date: Wed, 29 Aug 2018 11:04:29 +0100 Subject: [PATCH 09/10] proper parameter destructuring --- src/actions/index.js | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index c0ad0af..22673c7 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -114,14 +114,14 @@ export function fetchTEI(uri) { export function traverse( docUri, - params = { - objectPrefixWhitelist:[], objectUriWhitelist:[], objectTypeWhitelist : [], - objectPrefixBlacklist:[], objectUriBlacklist:[], objectTypeBlacklist : [], - propertyPrefixWhitelist:[], propertyUriWhitelist:[], - propertyPrefixBlacklist:[], propertyUriBlacklist:[], - objectives:{}, numHops:MAX_TRAVERSAL_HOPS, - useEtag : false, etag:"" - }) { + { // use destructuring to simulate named parameters + objectPrefixWhitelist=[], objectUriWhitelist=[], objectTypeWhitelist = [], + objectPrefixBlacklist=[], objectUriBlacklist=[], objectTypeBlacklist = [], + propertyPrefixWhitelist=[], propertyUriWhitelist=[], + propertyPrefixBlacklist=[], propertyUriBlacklist=[], + objectives={}, numHops=MAX_TRAVERSAL_HOPS, + useEtag = false, etag="" + } = {}) { // PURPOSE: // ************************************************************************* // Traverse through a graph, looking for entities of interest @@ -160,10 +160,20 @@ export function traverse( // properties with URIs that are NOT in the list. // ************************************************************************* + // create params object to pass on in recursive calls + // n.b. must update here if function signature changes + let params = { + objectPrefixWhitelist, objectUriWhitelist, objectTypeWhitelist, + objectPrefixBlacklist, objectUriBlacklist, objectTypeBlacklist, + propertyPrefixWhitelist, propertyUriWhitelist, + propertyPrefixBlacklist, propertyUriBlacklist, + objectives, numHops, + useEtag, etag + } // set up HTTP request const headers = {'Accept': 'application/ld+json'}; - if(params["useEtag"]) { - headers['If-None-Match'] = params["etag"]; + if(useEtag) { + headers['If-None-Match'] = etag; } console.log("FETCHING: ", docUri, params); @@ -229,11 +239,11 @@ export function traverse( console.log("<>", subjectUri, pred, obj["@id"], docUri); // Now recurse (if black/whitelist conditions and hop counter allow) // Remember that we've already visited the current document to avoid loops - if( params["numHops"] !== 0 && !(params["objectUriBlacklist"].includes(obj["@id"])) ) { + if( numHops !== 0 && !(objectUriBlacklist.includes(obj["@id"])) ) { dispatch(traverse(obj["@id"], { ...params, - "objectUriBlacklist": params["objectUriBlacklist"].concat(docUri), - "numHops": params["numHops"]-1 + "objectUriBlacklist": objectUriBlacklist.concat(docUri), + "numHops": numHops-1 })) } } else { From 83729674e13a8d213ffd8f86b66556d3932127f5 Mon Sep 17 00:00:00 2001 From: "David M. Weigl" Date: Mon, 3 Sep 2018 14:14:26 +0100 Subject: [PATCH 10/10] implement objectUriBlacklist check --- src/actions/index.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index 22673c7..6074ac7 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -240,11 +240,16 @@ export function traverse( // Now recurse (if black/whitelist conditions and hop counter allow) // Remember that we've already visited the current document to avoid loops if( numHops !== 0 && !(objectUriBlacklist.includes(obj["@id"])) ) { - dispatch(traverse(obj["@id"], { - ...params, - "objectUriBlacklist": objectUriBlacklist.concat(docUri), - "numHops": numHops-1 - })) + const badPrefixMatches = objectPrefixBlacklist.filter((b) => { + return obj["@id"].startsWith(b) + }) + if(badPrefixMatches.length === 0) { + dispatch(traverse(obj["@id"], { + ...params, + "objectUriBlacklist": objectUriBlacklist.concat(docUri), + "numHops": numHops-1 + })) + } } } else { // our *RDF* object is a literal