From dfc000adbbccd926d6017a4ac4b9db69ca7b1f3e Mon Sep 17 00:00:00 2001 From: Benjamin Degenhart Date: Tue, 28 May 2024 21:28:26 +0200 Subject: [PATCH] Add js files to structure the code base better --- public/dom-building.js | 207 ++++++++++++++++++ public/index.html | 470 +---------------------------------------- public/main.js | 22 ++ public/parser.js | 34 +++ public/profile.js | 79 +++++++ public/utils.js | 28 +++ public/validation.js | 95 +++++++++ 7 files changed, 470 insertions(+), 465 deletions(-) create mode 100644 public/dom-building.js create mode 100644 public/main.js create mode 100644 public/parser.js create mode 100644 public/profile.js create mode 100644 public/validation.js diff --git a/public/dom-building.js b/public/dom-building.js new file mode 100644 index 0000000..41db48c --- /dev/null +++ b/public/dom-building.js @@ -0,0 +1,207 @@ + +async function buildRemovalCell(td, sKey, pKey) { + td.textContent = "x" + td.addEventListener("click", async function() { + if (confirm("Do you want to remove this entry?")) { + console.log("Removing entry:", sKey, pKey) + searchSubjectNodeRecursively(userProfile, sKey, (node) => delete node[pKey]) + await finalizeProfileChanges() + } + }) +} + +function buildProfileTableRecursively(node, depth, table) { + let subject = node["@id"] + for (let [predicate, objectOrArray] of Object.entries(node)) { + if (predicate.startsWith("@")) continue + if (!Array.isArray(objectOrArray)) { + let tds = buildRowAndColumns(table) + let label = determineLabelForTableEntries(predicate) + tds[depth].textContent = label + tds[depth].title = predicate + tds[depth + 1].textContent = determineLabelForTableEntries(objectOrArray) + tds[depth + 1].title = objectOrArray + tds[depth + 1].addEventListener("click", async function() { + let input = prompt(`Enter new value for ${label}`) + if (input !== null) { + console.log("Changing entry value:", subject, predicate, "-->", input) + searchSubjectNodeRecursively(userProfile, subject, (node) => node[predicate] = input) + await finalizeProfileChanges() + } + }) + buildRemovalCell(tds[maxDepth + 2], subject, predicate) + continue + } + for (let arrayElement of objectOrArray) { + let tds = buildRowAndColumns(table) + tds[depth].textContent = dfShortUriToLabel(predicate) + tds[depth].title = predicate + // this will delete all array elements of the same type at the moment TODO fix + buildRemovalCell(tds[maxDepth + 2], subject, predicate) + buildProfileTableRecursively(arrayElement, depth + 1, table) + } + } +} + +// this is needed to know the correct number of columns in the profile table in advance +function determineDepthOfProfileTreeRecursively(jsonNode, depth) { + if (depth > maxDepth) maxDepth = depth + for (let objectOrArray of Object.values(jsonNode)) { + if (!Array.isArray(objectOrArray)) continue + for (let arrayElement of objectOrArray) determineDepthOfProfileTreeRecursively(arrayElement, depth + 1) + } +} + +function buildProfileTable() { + let div = document.getElementById("userProfileDiv") + div.innerHTML = "" + let table = document.createElement("table") + table.className = "framed-table" + div.appendChild(table) + maxDepth = 0 + determineDepthOfProfileTreeRecursively(userProfile, 0) + buildProfileTableRecursively(userProfile, 0, table) +} + +function buildFocusInputSelectChoices() { + let categories = {} + let rps = [] + for (let rp of Object.values(metadata.rp)) { + rps.push({ + value: rp.uri, + label: rp.title, + }) + for (let cat of rp.categories) { + if (!categories[cat]) categories[cat] = [] + categories[cat].push(rp.uri) + } + } + metadata.categories = categories + focusInputSelect.setChoices([ + { + label: "Categories", + choices: Object.keys(categories).map(uri => { + return { + value: uri, + label: uri.split("#")[1], + } + }) + }, + { + label: "Requirement Profiles", + choices: rps + }, + ]) +} + +async function buildPrioritizedMissingDataList() { + let div = document.getElementById("missingDataPointsDiv") + div.textContent = "" + let prioritizedList = [] + + let missingData = validateAllReport.missingUserInputsAggregated + + let focusRPs = [] + for (let focusItem of focusInputSelect.getValue(true)) { + if (metadata.categories[focusItem]) { + focusRPs = focusRPs.concat(metadata.categories[focusItem]) + } else { + focusRPs.push(focusItem) + } + } + + for (let datafield of Object.values(missingData)) { + let usedInRpUris = datafield.usedIn.map(usedInRP => usedInRP.rpUri) + if (focusRPs.length > 0 && !usedInRpUris.some(rpUri => focusRPs.includes(rpUri))) { + continue + } + + let usedInTitles = [] + let lastMissingCounter = 0 + for (let usedInRP of datafield.usedIn) { + usedInTitles.push(metadata.rp[usedInRP.rpUri].title + (usedInRP.isLastMissingUserInput ? " (!)" : "")) + if (usedInRP.isLastMissingUserInput) lastMissingCounter += 1 + } + prioritizedList.push({ + subject: datafield.subject, + dfUri: datafield.dfUri, + objectHasClass: metadata.df[datafield.dfUri]?.objectHasClass, + label: metadata.df[datafield.dfUri]?.label ?? datafield.dfUri.split("#")[1], + score: datafield.usedIn.length + lastMissingCounter, + usedInTitles: usedInTitles + }) + } + prioritizedList.sort((a, b) => b.score - a.score) + prioritizedList.forEach((entry) => { // entry = wrapped datafield + let spanEl = document.createElement("span") + spanEl.title = entry.usedInTitles.join("\n") + spanEl.style.color = "gray" + let textNode = document.createTextNode(entry.score + ": ") + spanEl.appendChild(textNode) + div.appendChild(spanEl) + spanEl = document.createElement("a") + spanEl.textContent = collectSubjectClassLabelsAlongPath(entry) + searchNodeByEntryPredicateRecursively(userProfile, "ff:hasDeferred", (node) => { + node["ff:hasDeferred"].forEach(deferment => { + if (deferment["rdf:subject"] === entry.subject && deferment["rdf:predicate"] === entry.dfUri) { + spanEl.style.textDecoration = "line-through" + } + }) + }) + spanEl.addEventListener("click", async function(event) { + event.preventDefault() + if (entry.objectHasClass) { + if (confirm("Do you want to add a " + metadata.df[entry.objectHasClass].label + "?")) { + instantiateNewObjectClassUnderSubject(entry.subject, entry.dfUri, entry.objectHasClass) + await finalizeProfileChanges() + } + return + } + let input = prompt("What is your value for: " + entry.label) + if (input !== null) { + addEntryToSubject(entry.subject, entry.dfUri, input) + await finalizeProfileChanges() + } + }) + div.appendChild(spanEl) + spanEl = document.createElement("span") + spanEl.style.fontSize = "x-small" + spanEl.style.color = "silver" + spanEl.innerHTML = "  defer this" + spanEl.addEventListener("click", async function() { + addDeferment(entry.subject, entry.dfUri) + await finalizeProfileChanges() + }) + div.appendChild(spanEl) + div.appendChild(document.createElement("br")) + }) + for (const elem of Array.from(document.getElementsByClassName("loadingDiv"))) { + elem.style.display = "none" + } +} + +function collectSubjectClassLabelsAlongPath(entry) { // prioritizedListEntry + if (shortenLongUri(entry.subject) === "ff:mainPerson") { + return entry.label + } + let sUri = shortenLongUri(entry.subject) + let path = "" + let finalPath + function recurse(node, arrIndex, sUri, path) { + if (node["@type"] !== "ff:Citizen") path += ", " + metadata.df[expandShortUri(node["@type"])].label + " " + arrIndex + for (let objectOrArray of Object.values(node)) { + if (objectOrArray === sUri) { + finalPath = path.substring(1) + } + if (Array.isArray(objectOrArray)) { + for (let i = 0; i < objectOrArray.length; i++) { + let arrayEl = objectOrArray[i] + recurse(arrayEl, i, sUri, path) + } + } + } + + } + recurse(userProfile, 0, sUri, path) + return finalPath + ": " + entry.label +} diff --git a/public/index.html b/public/index.html index 866e601..d341fd2 100644 --- a/public/index.html +++ b/public/index.html @@ -4,6 +4,11 @@ FörderFunke + + + + + @@ -64,471 +69,6 @@

Report

await buildPrioritizedMissingDataList() }, false) - const EMPTY_PROFILE = { "@id": "ff:mainPerson", "@type": "ff:Citizen" } - let userProfile - let maxDepth = 0 - let latestRPsRepoCommit - let turtleMap - let metadata - let validateAllReport - let eligibleRPs - - async function parseTurtleFiles() { - turtleMap = { - "datafields": await fetchAsset("requirement-profiles/datafields.ttl"), - "materialization": await fetchAsset("requirement-profiles/materialization.ttl"), - "shacl": {} - } - const shaclListCsv = await fetchAsset("shacl-list.csv") - for (let line of shaclListCsv.split("\n")) { - let [filename, rpUri] = line.split(",") - rpUri = expandShortUri(rpUri) - turtleMap.shacl[rpUri] = await fetchAsset("requirement-profiles/shacl/" + filename) - } - metadata = { - df: await MatchingEngine.extractDatafieldsMetadata(turtleMap.datafields), - rp: await MatchingEngine.extractRequirementProfilesMetadata(Object.values(turtleMap.shacl)) - } - - const selectEl = document.getElementById("dfDropdown") - selectEl.innerHTML = "" - for (let df of Object.values(metadata.df)) { - const optionEl = document.createElement("option") - optionEl.value = df.uri - optionEl.textContent = df.label - selectEl.appendChild(optionEl) - } - selectEl.addEventListener("change", function(event) { - const selectedValue = event.target.value - // TODO - }) - - console.log("metadata", metadata) - buildFocusInputSelectChoices() - } - - function buildFocusInputSelectChoices() { - let categories = {} - let rps = [] - for (let rp of Object.values(metadata.rp)) { - rps.push({ - value: rp.uri, - label: rp.title, - }) - for (let cat of rp.categories) { - if (!categories[cat]) categories[cat] = [] - categories[cat].push(rp.uri) - } - } - metadata.categories = categories - focusInputSelect.setChoices([ - { - label: "Categories", - choices: Object.keys(categories).map(uri => { - return { - value: uri, - label: uri.split("#")[1], - } - }) - }, - { - label: "Requirement Profiles", - choices: rps - }, - ]) - } - - async function update() { - await buildProfileTable() - - let userProfileTurtle = await MatchingEngine.convertUserProfileToTurtle(userProfile) - console.log("userProfileTurtle", userProfileTurtle) - validateAllReport = await MatchingEngine.validateAll(userProfileTurtle, turtleMap.shacl, turtleMap.datafields, turtleMap.materialization) - console.log("validateAllReport", validateAllReport) - - let tableEl = document.getElementById("reportTable") - tableEl.textContent = "" - eligibleRPs = [] - - for (let report of validateAllReport.reports) { - if (report.result === "eligible") { - eligibleRPs.push(report.rpUri) - } - let tr = document.createElement("tr") - let td = document.createElement("td") - td.textContent = getRpTitle(report.rpUri) - searchNodeByEntryPredicateRecursively(userProfile, "ff:hasCompliedRequirementProfile", (node) => { - node["ff:hasCompliedRequirementProfile"].forEach(rp => { - if (rp["ff:hasRpUri"] === report.rpUri) { - td.style.textDecoration = "line-through" - } - }) - }) - if (report.containsDeferredMissingUserInput) { - td.style.textDecoration = "line-through" - } - if (!metadata.rp[report.rpUri] || metadata.rp[report.rpUri].categories.length === 0) { - td.title = "No category info available" - } else { - td.title = metadata.rp[report.rpUri].categories.map(cat => cat.split("#")[1]).join("\n") - } - tr.appendChild(td) - td = document.createElement("td") - td.textContent = report.result - let msg = "" - switch (report.result) { - case "eligible": - td.style.color = "green" - td.style.fontWeight = "bold" - msg += JSON.stringify(report.materializationReport) // TODO - break - case "ineligible": - td.style.color = "red" - for (let violation of report.violations) { - msg += violation.message + "\n" - } - break - case "undeterminable": - td.style.color = "gray" - msg += "Missing data points:\n" - for (let missing of report.missingUserInput) { - if (metadata.df[missing.dfUri]) { - msg += "- " + metadata.df[missing.dfUri].label + "\n" - } - } - break - } - td.title = msg - tr.appendChild(td) - td = document.createElement("td") - td.style.fontSize = "x-small" - td.style.color = "silver" - td.innerHTML = "   already getting this" - td.addEventListener("click", async function() { - let newInstanceUri = instantiateNewObjectClassUnderSubject("ff:mainPerson", "ff:hasCompliedRequirementProfile", "ff:CompliedRequirementProfile") - addEntryToSubject(newInstanceUri, "ff:hasRpUri", report.rpUri) - - let userProfileTurtle = await MatchingEngine.convertUserProfileToTurtle(userProfile) - let inferenceReport = await MatchingEngine.inferNewUserDataFromCompliedRPs(userProfileTurtle, turtleMap.shacl[report.rpUri]) - let msg = "" - let triples = [] - for (let triple of inferenceReport.triples) { - if (!triple.deferredBy) { - msg += shortenLongUri(triple.s) + " " + shortenLongUri(triple.p) + " " + shortenLongUri(triple.o) + "\n" - triples.push(triple) - } - } - if (triples.length > 0 && confirm("These entries were inferred based on your already complied requirement profile '" + metadata.rp[report.rpUri].title - + "':\n\n" + msg + "\nWould you like to add them to your profile?")) { - for (let triple of triples) { - addEntryToSubject(triple.s, triple.p, triple.o) - } - } // else {}: should we offer to add deferments if they choose not to? But it won't pop up again anyway since inferNewUserDataFromCompliedRPs() is not called regularly - await finalizeProfileChanges() - }) - tr.appendChild(td) - tableEl.appendChild(tr) - } - - await buildPrioritizedMissingDataList() - } - - async function buildPrioritizedMissingDataList() { - let div = document.getElementById("missingDataPointsDiv") - div.textContent = "" - let prioritizedList = [] - - let missingData = validateAllReport.missingUserInputsAggregated - - let focusRPs = [] - for (let focusItem of focusInputSelect.getValue(true)) { - if (metadata.categories[focusItem]) { - focusRPs = focusRPs.concat(metadata.categories[focusItem]) - } else { - focusRPs.push(focusItem) - } - } - - for (let datafield of Object.values(missingData)) { - let usedInRpUris = datafield.usedIn.map(usedInRP => usedInRP.rpUri) - if (focusRPs.length > 0 && !usedInRpUris.some(rpUri => focusRPs.includes(rpUri))) { - continue - } - - let usedInTitles = [] - let lastMissingCounter = 0 - for (let usedInRP of datafield.usedIn) { - usedInTitles.push(metadata.rp[usedInRP.rpUri].title + (usedInRP.isLastMissingUserInput ? " (!)" : "")) - if (usedInRP.isLastMissingUserInput) lastMissingCounter += 1 - } - prioritizedList.push({ - subject: datafield.subject, - dfUri: datafield.dfUri, - objectHasClass: metadata.df[datafield.dfUri]?.objectHasClass, - label: metadata.df[datafield.dfUri]?.label ?? datafield.dfUri.split("#")[1], - score: datafield.usedIn.length + lastMissingCounter, - usedInTitles: usedInTitles - }) - } - prioritizedList.sort((a, b) => b.score - a.score) - prioritizedList.forEach((entry) => { // entry = wrapped datafield - let spanEl = document.createElement("span") - spanEl.title = entry.usedInTitles.join("\n") - spanEl.style.color = "gray" - let textNode = document.createTextNode(entry.score + ": ") - spanEl.appendChild(textNode) - div.appendChild(spanEl) - spanEl = document.createElement("a") - spanEl.textContent = collectSubjectClassLabelsAlongPath(entry) - searchNodeByEntryPredicateRecursively(userProfile, "ff:hasDeferred", (node) => { - node["ff:hasDeferred"].forEach(deferment => { - if (deferment["rdf:subject"] === entry.subject && deferment["rdf:predicate"] === entry.dfUri) { - spanEl.style.textDecoration = "line-through" - } - }) - }) - spanEl.addEventListener("click", async function(event) { - event.preventDefault() - if (entry.objectHasClass) { - if (confirm("Do you want to add a " + metadata.df[entry.objectHasClass].label + "?")) { - instantiateNewObjectClassUnderSubject(entry.subject, entry.dfUri, entry.objectHasClass) - await finalizeProfileChanges() - } - return - } - let input = prompt("What is your value for: " + entry.label) - if (input !== null) { - addEntryToSubject(entry.subject, entry.dfUri, input) - await finalizeProfileChanges() - } - }) - div.appendChild(spanEl) - spanEl = document.createElement("span") - spanEl.style.fontSize = "x-small" - spanEl.style.color = "silver" - spanEl.innerHTML = "  defer this" - spanEl.addEventListener("click", async function() { - addDeferment(entry.subject, entry.dfUri) - await finalizeProfileChanges() - }) - div.appendChild(spanEl) - div.appendChild(document.createElement("br")) - }) - for (const elem of Array.from(document.getElementsByClassName("loadingDiv"))) { - elem.style.display = "none" - } - } - - function collectSubjectClassLabelsAlongPath(entry) { // prioritizedListEntry - if (shortenLongUri(entry.subject) === "ff:mainPerson") { - return entry.label - } - let sUri = shortenLongUri(entry.subject) - let path = "" - let finalPath - function recurse(node, arrIndex, sUri, path) { - if (node["@type"] !== "ff:Citizen") path += ", " + metadata.df[expandShortUri(node["@type"])].label + " " + arrIndex - for (let objectOrArray of Object.values(node)) { - if (objectOrArray === sUri) { - finalPath = path.substring(1) - } - if (Array.isArray(objectOrArray)) { - for (let i = 0; i < objectOrArray.length; i++) { - let arrayEl = objectOrArray[i] - recurse(arrayEl, i, sUri, path) - } - } - } - - } - recurse(userProfile, 0, sUri, path) - return finalPath + ": " + entry.label - } - - function searchNodeByEntryPredicateRecursively(node, predicateValue, action) { - for (let [predicate, objectOrArray] of Object.entries(node)) { - if (predicate === predicateValue) { // e.g. ff:hasChild as the key - action(node) - return - } - if (Array.isArray(objectOrArray)) { - for (let arrayEl of objectOrArray) { - searchNodeByEntryPredicateRecursively(arrayEl, predicateValue, action) - } - } - } - } - - function addEntryToSubject(subject, predicate, objectValue) { - subject = shortenLongUri(subject) - predicate = shortenLongUri(predicate) - console.log("Adding entry:", subject, predicate, "-->", objectValue) - searchSubjectNodeRecursively(userProfile, subject, (node) => node[predicate] = objectValue) - } - - function instantiateNewObjectClassUnderSubject(subject, predicate, objectClass) { - console.log("Adding object class instantiation:", subject, predicate, "-->", objectClass) - let shortObjectClassUri = shortenLongUri(objectClass) - let newInstanceUri = shortObjectClassUri.toLowerCase() - let nodeFound = false - searchNodeByEntryPredicateRecursively(userProfile, predicate, (node) => { - nodeFound = true - newInstanceUri = newInstanceUri + node[predicate].length - node[predicate].push({ "@id": newInstanceUri, "@type": shortObjectClassUri }) // verify that this works TODO - }) - if (!nodeFound) { // e.g. no ff:hasChild array yet - newInstanceUri = newInstanceUri + "0" - addEntryToSubject(subject, predicate, [{ "@id": newInstanceUri, "@type": shortObjectClassUri }]) - } - return newInstanceUri - } - - async function finalizeProfileChanges() { - if (!await validateUserProfile()) return - - let userProfileTurtle = await MatchingEngine.convertUserProfileToTurtle(userProfile) - let materializationReport = await MatchingEngine.checkUserProfileForMaterializations(userProfileTurtle, turtleMap.materialization) - // run inferNewUserDataFromCompliedRPs() here too? - console.log("materializationReport", materializationReport) - - let msg = "" - let triples = [] - for (let round of materializationReport.rounds) { - for (let [ruleUri, entry] of Object.entries(round)) { - let ruleLocalName = ruleUri.split("#")[1] - msg += "Via materialization rule '" + ruleLocalName + "' (input: " - msg += entry.input ? metadata.df[entry.input].label : "?" - msg += " output: " + (entry.output ? metadata.df[entry.output].label : "?") + "):\n" - for (let triple of entry.triples) { - if (!triple.deferredBy) { - msg += shortenLongUri(triple.s) + " " + shortenLongUri(triple.p) + " " + shortenLongUri(triple.o) + "\n" - triples.push(triple) - } - } - } - } - if (triples.length > 0) { - if (confirm("The following entries where inferred from your existing entries:\n\n" + msg + "\nWould you like to add them to your profile?")) { - for (let triple of triples) { - addEntryToSubject(triple.s, triple.p, triple.o) - } - if (!await validateUserProfile()) return - } else { - if (!confirm("May I ask you later again to add these? ")) { - for (let triple of triples) { - addDeferment(triple.s, triple.p) - } - } - } - } - console.log("userProfile", userProfile) - localStorage.setItem("userProfile", JSON.stringify(userProfile)) - await update() - } - - async function validateUserProfile() { - let userProfileTurtle = await MatchingEngine.convertUserProfileToTurtle(userProfile) - let report = await MatchingEngine.validateUserProfile(userProfileTurtle, turtleMap.datafields) - if (!report.conforms) { - console.log("User profile validation violations:", report.violations) - // pretty print violations TODO - alert("Your profile is not valid: " + JSON.stringify(report.violations)) - return false - } - return true - } - - function searchSubjectNodeRecursively(node, sKey, action) { - if (node["@id"] === sKey) { - action(node) - return - } - for (let objectOrArray of Object.values(node)) { - if (Array.isArray(objectOrArray)) { - for (let arrayEl of objectOrArray) { - searchSubjectNodeRecursively(arrayEl, sKey, action) - } - } - } - } - - async function buildRemovalCell(td, sKey, pKey) { - td.textContent = "x" - td.addEventListener("click", async function() { - if (confirm("Do you want to remove this entry?")) { - console.log("Removing entry:", sKey, pKey) - searchSubjectNodeRecursively(userProfile, sKey, (node) => delete node[pKey]) - await finalizeProfileChanges() - } - }) - } - - function buildProfileTableRecursively(node, depth, table) { - let subject = node["@id"] - for (let [predicate, objectOrArray] of Object.entries(node)) { - if (predicate.startsWith("@")) continue - if (!Array.isArray(objectOrArray)) { - let tds = buildRowAndColumns(table) - let label = determineLabelForTableEntries(predicate) - tds[depth].textContent = label - tds[depth].title = predicate - tds[depth + 1].textContent = determineLabelForTableEntries(objectOrArray) - tds[depth + 1].title = objectOrArray - tds[depth + 1].addEventListener("click", async function() { - let input = prompt(`Enter new value for ${label}`) - if (input !== null) { - console.log("Changing entry value:", subject, predicate, "-->", input) - searchSubjectNodeRecursively(userProfile, subject, (node) => node[predicate] = input) - await finalizeProfileChanges() - } - }) - buildRemovalCell(tds[maxDepth + 2], subject, predicate) - continue - } - for (let arrayElement of objectOrArray) { - let tds = buildRowAndColumns(table) - tds[depth].textContent = dfShortUriToLabel(predicate) - tds[depth].title = predicate - // this will delete all array elements of the same type at the moment TODO fix - buildRemovalCell(tds[maxDepth + 2], subject, predicate) - buildProfileTableRecursively(arrayElement, depth + 1, table) - } - } - } - - // this is needed to know the correct number of columns in the profile table in advance - function determineDepthOfProfileTreeRecursively(jsonNode, depth) { - if (depth > maxDepth) maxDepth = depth - for (let objectOrArray of Object.values(jsonNode)) { - if (!Array.isArray(objectOrArray)) continue - for (let arrayElement of objectOrArray) determineDepthOfProfileTreeRecursively(arrayElement, depth + 1) - } - } - - function buildProfileTable() { - let div = document.getElementById("userProfileDiv") - div.innerHTML = "" - let table = document.createElement("table") - table.className = "framed-table" - div.appendChild(table) - maxDepth = 0 - determineDepthOfProfileTreeRecursively(userProfile, 0) - buildProfileTableRecursively(userProfile, 0, table) - } - - async function run() { - latestRPsRepoCommit = await fetchAsset("latest-rps-repo-commit.txt") - setInterval(checkForNewRepoCommits, 60 * 1000) - await parseTurtleFiles() - - if (localStorage.getItem("userProfile") === null) { - localStorage.setItem("userProfile", JSON.stringify(EMPTY_PROFILE)) - } - userProfile = JSON.parse(localStorage.getItem("userProfile")) - if (!await validateUserProfile()) return - await update() - } - run() diff --git a/public/main.js b/public/main.js new file mode 100644 index 0000000..f45fc9b --- /dev/null +++ b/public/main.js @@ -0,0 +1,22 @@ + +const EMPTY_PROFILE = { "@id": "ff:mainPerson", "@type": "ff:Citizen" } +let userProfile +let maxDepth = 0 +let latestRPsRepoCommit +let turtleMap +let metadata +let validateAllReport +let eligibleRPs + +async function run() { + latestRPsRepoCommit = await fetchAsset("latest-rps-repo-commit.txt") + setInterval(checkForNewRepoCommits, 60 * 1000) + await parseTurtleFiles() + + if (localStorage.getItem("userProfile") === null) { + localStorage.setItem("userProfile", JSON.stringify(EMPTY_PROFILE)) + } + userProfile = JSON.parse(localStorage.getItem("userProfile")) + if (!await validateUserProfile()) return + await update() +} diff --git a/public/parser.js b/public/parser.js new file mode 100644 index 0000000..442e767 --- /dev/null +++ b/public/parser.js @@ -0,0 +1,34 @@ + +async function parseTurtleFiles() { + turtleMap = { + "datafields": await fetchAsset("requirement-profiles/datafields.ttl"), + "materialization": await fetchAsset("requirement-profiles/materialization.ttl"), + "shacl": {} + } + const shaclListCsv = await fetchAsset("shacl-list.csv") + for (let line of shaclListCsv.split("\n")) { + let [filename, rpUri] = line.split(",") + rpUri = expandShortUri(rpUri) + turtleMap.shacl[rpUri] = await fetchAsset("requirement-profiles/shacl/" + filename) + } + metadata = { + df: await MatchingEngine.extractDatafieldsMetadata(turtleMap.datafields), + rp: await MatchingEngine.extractRequirementProfilesMetadata(Object.values(turtleMap.shacl)) + } + + const selectEl = document.getElementById("dfDropdown") + selectEl.innerHTML = "" + for (let df of Object.values(metadata.df)) { + const optionEl = document.createElement("option") + optionEl.value = df.uri + optionEl.textContent = df.label + selectEl.appendChild(optionEl) + } + selectEl.addEventListener("change", function(event) { + const selectedValue = event.target.value + // TODO + }) + + console.log("metadata", metadata) + buildFocusInputSelectChoices() +} diff --git a/public/profile.js b/public/profile.js new file mode 100644 index 0000000..e04ef76 --- /dev/null +++ b/public/profile.js @@ -0,0 +1,79 @@ + +function addEntryToSubject(subject, predicate, objectValue) { + subject = shortenLongUri(subject) + predicate = shortenLongUri(predicate) + console.log("Adding entry:", subject, predicate, "-->", objectValue) + searchSubjectNodeRecursively(userProfile, subject, (node) => node[predicate] = objectValue) +} + +function instantiateNewObjectClassUnderSubject(subject, predicate, objectClass) { + console.log("Adding object class instantiation:", subject, predicate, "-->", objectClass) + let shortObjectClassUri = shortenLongUri(objectClass) + let newInstanceUri = shortObjectClassUri.toLowerCase() + let nodeFound = false + searchNodeByEntryPredicateRecursively(userProfile, predicate, (node) => { + nodeFound = true + newInstanceUri = newInstanceUri + node[predicate].length + node[predicate].push({ "@id": newInstanceUri, "@type": shortObjectClassUri }) // verify that this works TODO + }) + if (!nodeFound) { // e.g. no ff:hasChild array yet + newInstanceUri = newInstanceUri + "0" + addEntryToSubject(subject, predicate, [{ "@id": newInstanceUri, "@type": shortObjectClassUri }]) + } + return newInstanceUri +} + +async function finalizeProfileChanges() { + if (!await validateUserProfile()) return + + let userProfileTurtle = await MatchingEngine.convertUserProfileToTurtle(userProfile) + let materializationReport = await MatchingEngine.checkUserProfileForMaterializations(userProfileTurtle, turtleMap.materialization) + // run inferNewUserDataFromCompliedRPs() here too? + console.log("materializationReport", materializationReport) + + let msg = "" + let triples = [] + for (let round of materializationReport.rounds) { + for (let [ruleUri, entry] of Object.entries(round)) { + let ruleLocalName = ruleUri.split("#")[1] + msg += "Via materialization rule '" + ruleLocalName + "' (input: " + msg += entry.input ? metadata.df[entry.input].label : "?" + msg += " output: " + (entry.output ? metadata.df[entry.output].label : "?") + "):\n" + for (let triple of entry.triples) { + if (!triple.deferredBy) { + msg += shortenLongUri(triple.s) + " " + shortenLongUri(triple.p) + " " + shortenLongUri(triple.o) + "\n" + triples.push(triple) + } + } + } + } + if (triples.length > 0) { + if (confirm("The following entries where inferred from your existing entries:\n\n" + msg + "\nWould you like to add them to your profile?")) { + for (let triple of triples) { + addEntryToSubject(triple.s, triple.p, triple.o) + } + if (!await validateUserProfile()) return + } else { + if (!confirm("May I ask you later again to add these? ")) { + for (let triple of triples) { + addDeferment(triple.s, triple.p) + } + } + } + } + console.log("userProfile", userProfile) + localStorage.setItem("userProfile", JSON.stringify(userProfile)) + await update() +} + +async function validateUserProfile() { + let userProfileTurtle = await MatchingEngine.convertUserProfileToTurtle(userProfile) + let report = await MatchingEngine.validateUserProfile(userProfileTurtle, turtleMap.datafields) + if (!report.conforms) { + console.log("User profile validation violations:", report.violations) + // pretty print violations TODO + alert("Your profile is not valid: " + JSON.stringify(report.violations)) + return false + } + return true +} diff --git a/public/utils.js b/public/utils.js index 566302e..18d2cd9 100644 --- a/public/utils.js +++ b/public/utils.js @@ -1,4 +1,32 @@ +function searchNodeByEntryPredicateRecursively(node, predicateValue, action) { + for (let [predicate, objectOrArray] of Object.entries(node)) { + if (predicate === predicateValue) { // e.g. ff:hasChild as the key + action(node) + return + } + if (Array.isArray(objectOrArray)) { + for (let arrayEl of objectOrArray) { + searchNodeByEntryPredicateRecursively(arrayEl, predicateValue, action) + } + } + } +} + +function searchSubjectNodeRecursively(node, sKey, action) { + if (node["@id"] === sKey) { + action(node) + return + } + for (let objectOrArray of Object.values(node)) { + if (Array.isArray(objectOrArray)) { + for (let arrayEl of objectOrArray) { + searchSubjectNodeRecursively(arrayEl, sKey, action) + } + } + } +} + async function checkForNewRepoCommits() { console.log("Checking for updates, old commit:", latestRPsRepoCommit) let checkLatestRPsRepoCommit = await fetchAsset("latest-rps-repo-commit.txt") diff --git a/public/validation.js b/public/validation.js new file mode 100644 index 0000000..8c651d1 --- /dev/null +++ b/public/validation.js @@ -0,0 +1,95 @@ + +async function update() { + await buildProfileTable() + + let userProfileTurtle = await MatchingEngine.convertUserProfileToTurtle(userProfile) + console.log("userProfileTurtle", userProfileTurtle) + validateAllReport = await MatchingEngine.validateAll(userProfileTurtle, turtleMap.shacl, turtleMap.datafields, turtleMap.materialization) + console.log("validateAllReport", validateAllReport) + + let tableEl = document.getElementById("reportTable") + tableEl.textContent = "" + eligibleRPs = [] + + for (let report of validateAllReport.reports) { + if (report.result === "eligible") { + eligibleRPs.push(report.rpUri) + } + let tr = document.createElement("tr") + let td = document.createElement("td") + td.textContent = getRpTitle(report.rpUri) + searchNodeByEntryPredicateRecursively(userProfile, "ff:hasCompliedRequirementProfile", (node) => { + node["ff:hasCompliedRequirementProfile"].forEach(rp => { + if (rp["ff:hasRpUri"] === report.rpUri) { + td.style.textDecoration = "line-through" + } + }) + }) + if (report.containsDeferredMissingUserInput) { + td.style.textDecoration = "line-through" + } + if (!metadata.rp[report.rpUri] || metadata.rp[report.rpUri].categories.length === 0) { + td.title = "No category info available" + } else { + td.title = metadata.rp[report.rpUri].categories.map(cat => cat.split("#")[1]).join("\n") + } + tr.appendChild(td) + td = document.createElement("td") + td.textContent = report.result + let msg = "" + switch (report.result) { + case "eligible": + td.style.color = "green" + td.style.fontWeight = "bold" + msg += JSON.stringify(report.materializationReport) // TODO + break + case "ineligible": + td.style.color = "red" + for (let violation of report.violations) { + msg += violation.message + "\n" + } + break + case "undeterminable": + td.style.color = "gray" + msg += "Missing data points:\n" + for (let missing of report.missingUserInput) { + if (metadata.df[missing.dfUri]) { + msg += "- " + metadata.df[missing.dfUri].label + "\n" + } + } + break + } + td.title = msg + tr.appendChild(td) + td = document.createElement("td") + td.style.fontSize = "x-small" + td.style.color = "silver" + td.innerHTML = "   already getting this" + td.addEventListener("click", async function() { + let newInstanceUri = instantiateNewObjectClassUnderSubject("ff:mainPerson", "ff:hasCompliedRequirementProfile", "ff:CompliedRequirementProfile") + addEntryToSubject(newInstanceUri, "ff:hasRpUri", report.rpUri) + + let userProfileTurtle = await MatchingEngine.convertUserProfileToTurtle(userProfile) + let inferenceReport = await MatchingEngine.inferNewUserDataFromCompliedRPs(userProfileTurtle, turtleMap.shacl[report.rpUri]) + let msg = "" + let triples = [] + for (let triple of inferenceReport.triples) { + if (!triple.deferredBy) { + msg += shortenLongUri(triple.s) + " " + shortenLongUri(triple.p) + " " + shortenLongUri(triple.o) + "\n" + triples.push(triple) + } + } + if (triples.length > 0 && confirm("These entries were inferred based on your already complied requirement profile '" + metadata.rp[report.rpUri].title + + "':\n\n" + msg + "\nWould you like to add them to your profile?")) { + for (let triple of triples) { + addEntryToSubject(triple.s, triple.p, triple.o) + } + } // else {}: should we offer to add deferments if they choose not to? But it won't pop up again anyway since inferNewUserDataFromCompliedRPs() is not called regularly + await finalizeProfileChanges() + }) + tr.appendChild(td) + tableEl.appendChild(tr) + } + + await buildPrioritizedMissingDataList() +}