// Table Widget: Format an array of RDF statements as an HTML table. // // This can operate in one of three modes: when the class of object is given // or when the source document from whuch data is taken is given, // or if a prepared query object is given. // (In principle it could operate with neither class nor document // given but typically // there would be too much data.) // When the tableClass is not given, it looks for common classes in the data, // and gives the user the option. // // 2008 Written, Ilaria Liccardi as the tableViewPane.js // 2014 Core table widget moved into common/table.js - timbl // import * as debug from './debug' import { icons } from './iconBase' import { store } from 'solid-logic-jss' import * as log from './log' import ns from './ns' import * as rdf from 'rdflib' // pull in first avoid cross-refs import * as utils from './utils' import * as widgets from './widgets' const UI = { icons, log, ns, utils, widgets } // UI.widgets.renderTableViewPane export function renderTableViewPane (doc, options) { const sourceDocument = options.sourceDocument const tableClass = options.tableClass const givenQuery = options.query const RDFS_LITERAL = 'http://www.w3.org/2000/01/rdf-schema#Literal' const ns = UI.ns const kb = store const rowsLookup = {} // Persistent mapping of subject URI to dom TR // Predicates that are never made into columns: const FORBIDDEN_COLUMNS = { 'http://www.w3.org/2002/07/owl#sameAs': true, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type': true } // Number types defined in the XML schema: const XSD_NUMBER_TYPES = { 'http://www.w3.org/2001/XMLSchema#decimal': true, 'http://www.w3.org/2001/XMLSchema#float': true, 'http://www.w3.org/2001/XMLSchema#double': true, 'http://www.w3.org/2001/XMLSchema#integer': true, 'http://www.w3.org/2001/XMLSchema#nonNegativeInteger': true, 'http://www.w3.org/2001/XMLSchema#positiveInteger': true, 'http://www.w3.org/2001/XMLSchema#nonPositiveInteger': true, 'http://www.w3.org/2001/XMLSchema#negativeInteger': true, 'http://www.w3.org/2001/XMLSchema#long': true, 'http://www.w3.org/2001/XMLSchema#int': true, 'http://www.w3.org/2001/XMLSchema#short': true, 'http://www.w3.org/2001/XMLSchema#byte': true, 'http://www.w3.org/2001/XMLSchema#unsignedLong': true, 'http://www.w3.org/2001/XMLSchema#unsignedInt': true, 'http://www.w3.org/2001/XMLSchema#unsignedShort': true, 'http://www.w3.org/2001/XMLSchema#unsignedByte': true } const XSD_DATE_TYPES = { 'http://www.w3.org/2001/XMLSchema#dateTime': true, 'http://www.w3.org/2001/XMLSchema#date': true } // Classes that indicate an image: const IMAGE_TYPES = { 'http://xmlns.com/foaf/0.1/Image': true, 'http://purl.org/dc/terms/Image': true } // Name of the column used as a "key" value to look up the row. // This is necessary because in the normal view, the columns are // all "optional" values, meaning that we will get a result set // for every individual value that is found. The row key acts // as an anchor that can be used to combine this information // back into the same row. const keyVariable = options.keyVariable || '?_row' let subjectIdCounter = 0 let allType, types let typeSelectorDiv, addColumnDiv // The last SPARQL query used: let lastQuery = null let mostCommonType = null const resultDiv = doc.createElement('div') resultDiv.className = 'tableViewPane' resultDiv.appendChild(generateControlBar()) // sets typeSelectorDiv const tableDiv = doc.createElement('div') resultDiv.appendChild(tableDiv) // Save a refresh function for use by caller resultDiv.refresh = function () { runQuery(table.query, table.logicalRows, table.columns, table) // updateTable(givenQuery, mostCommonType) // This could be a lot more incremental and efficient } // A specifically asked-for query let table if (givenQuery) { table = renderTableForQuery(givenQuery) // lastQuery = givenQuery tableDiv.appendChild(table) } else { // Find the most common type and select it by default const s = calculateTable() allType = s[0] types = s[1] if (!tableClass) { typeSelectorDiv.appendChild(generateTypeSelector(allType, types)) } mostCommonType = getMostCommonType(types) if (mostCommonType) { buildFilteredTable(mostCommonType) } else { buildFilteredTable(allType) } } return resultDiv // ///////////////////////////////////////////////////////////////// /* function closeDialog (dialog) { dialog.parentNode.removeChild(dialog) } function createActionButton (label, callback) { var button = doc.createElement('input') button.setAttribute('type', 'submit') button.setAttribute('value', label) button.addEventListener('click', callback, false) return button } // @@ Tdo: put these buttonsback in, // to allow user to see and edit and save the sparql query for the table they are looking at // function createSparqlWindow () { var dialog = doc.createElement('div') dialog.setAttribute('class', 'sparqlDialog') var title = doc.createElement('h3') title.appendChild(doc.createTextNode('Edit SPARQL query')) var inputbox = doc.createElement('textarea') inputbox.value = rdf.queryToSPARQL(lastQuery) dialog.appendChild(title) dialog.appendChild(inputbox) dialog.appendChild(createActionButton('Query', function () { var query = rdf.SPARQLToQuery(inputbox.value) updateTable(query) closeDialog(dialog) })) dialog.appendChild(createActionButton('Close', function () { closeDialog(dialog) })) return dialog } function sparqlButtonPressed () { var dialog = createSparqlWindow() resultDiv.appendChild(dialog) } function generateSparqlButton () { var image = doc.createElement('img') image.setAttribute('class', 'sparqlButton') image.setAttribute('src', UI.iconBase + 'icons/1pt5a.gif') image.setAttribute('alt', 'Edit SPARQL query') image.addEventListener('click', sparqlButtonPressed, false) return image } */ // Generate the control bar displayed at the top of the screen. function generateControlBar () { const result = doc.createElement('table') result.setAttribute('class', 'toolbar') const tr = doc.createElement('tr') /* @@ Add in later -- not debugged yet var sparqlButtonDiv = doc.createElement("td") sparqlButtonDiv.appendChild(generateSparqlButton()) tr.appendChild(sparqlButtonDiv) */ typeSelectorDiv = doc.createElement('td') tr.appendChild(typeSelectorDiv) addColumnDiv = doc.createElement('td') tr.appendChild(addColumnDiv) result.appendChild(tr) return result } // Add the SELECT details to the query being built. function addSelectToQuery (query, type) { const selectedColumns = type.getColumns() for (let i = 0; i < selectedColumns.length; ++i) { // TODO: autogenerate nicer names for variables // variables have to be unambiguous const variable = kb.variable('_col' + i) query.vars.push(variable) selectedColumns[i].setVariable(variable) } } // Add WHERE details to the query being built. function addWhereToQuery (query, rowVar, type) { let queryType = type.type if (!queryType) { queryType = kb.variable('_any') } // _row a type query.pat.add(rowVar, UI.ns.rdf('type'), queryType) } // Generate OPTIONAL column selectors. function addColumnsToQuery (query, rowVar, type) { const selectedColumns = type.getColumns() for (let i = 0; i < selectedColumns.length; ++i) { const column = selectedColumns[i] const formula = kb.formula() formula.add(rowVar, column.predicate, column.getVariable()) query.pat.optional.push(formula) } } // Generate a query object from the currently-selected type // object. function generateQuery (type) { const query = new rdf.Query() const rowVar = kb.variable(keyVariable.slice(1)) // don't pass '?' addSelectToQuery(query, type) addWhereToQuery(query, rowVar, type) addColumnsToQuery(query, rowVar, type) return query } // Build the contents of the tableDiv element, filtered according // to the specified type. function buildFilteredTable (type) { // Generate "add column" cell. clearElement(addColumnDiv) addColumnDiv.appendChild(generateColumnAddDropdown(type)) const query = generateQuery(type) updateTable(query, type) } function updateTable (query, type) { // Stop the previous query from doing any updates. if (lastQuery) { lastQuery.running = false } // Render the HTML table. const htmlTable = renderTableForQuery(query, type) // Clear the tableDiv element, and replace with the new table. clearElement(tableDiv) tableDiv.appendChild(htmlTable) // Save the query for the edit dialog. lastQuery = query } // Remove all subelements of the specified element. function clearElement (element) { while (element.childNodes.length > 0) { element.removeChild(element.childNodes[0]) } } // A SubjectType is created for each rdf:type discovered. function SubjectType (type) { this.type = type this.columns = null this.allColumns = [] this.useCount = 0 // Get a list of all columns used by this type. this.getAllColumns = function () { return this.allColumns } // Get a list of the current columns used by this type // (subset of allColumns) this.getColumns = function () { // The first time through, get a list of all the columns // and select only the six most popular columns. if (!this.columns) { const allColumns = this.getAllColumns() this.columns = allColumns.slice(0, 7) } return this.columns } // Get a list of unused columns this.getUnusedColumns = function () { const allColumns = this.getAllColumns() const columns = this.getColumns() const result = [] for (let i = 0; i < allColumns.length; ++i) { if (columns.indexOf(allColumns[i]) === -1) { result.push(allColumns[i]) } } return result } this.addColumn = function (column) { this.columns.push(column) } this.removeColumn = function (column) { this.columns = this.columns.filter(function (x) { return x !== column }) } this.getLabel = function () { return utils.label(this.type) } this.addUse = function () { this.useCount += 1 } } // Class representing a column in the table. function Column () { this.useCount = 0 // Have we checked any values for this column yet? this.checkedAnyValues = false // If the range is unknown, but we just get literals in this // column, then we can generate a literal selector. this.possiblyLiteral = true // If the range is unknown, but we just get literals and they // match the regular expression for numbers, we can generate // a number selector. this.possiblyNumber = true // We accumulate classes which things in the column must be a member of this.constraints = [] // Check values as they are read. If we don't know what the // range is, we might be able to infer that it is a literal // if all of the values are literals. Similarly, we might // be able to determine if the literal values are actually // numbers (using regexps). this.checkValue = function (term) { const termType = term.termType if ( this.possiblyLiteral && termType !== 'Literal' && termType !== 'NamedNode' ) { this.possiblyNumber = false this.possiblyLiteral = false } else if (this.possiblyNumber) { if (termType !== 'Literal') { this.possiblyNumber = false } else { const literalValue = term.value if (!literalValue.match(/^-?\d+(\.\d*)?$/)) { this.possiblyNumber = false } } } this.checkedAnyValues = true } this.getVariable = function () { return this.variable } this.setVariable = function (variable) { this.variable = variable } this.getKey = function () { return this.variable.toString() } this.addUse = function () { this.useCount += 1 } this.getHints = function () { if ( options && options.hints && this.variable && options.hints[this.variable.toNT()] ) { return options.hints[this.variable.toNT()] } return {} } this.getLabel = function () { if (this.getHints().label) { return this.getHints().label } if (this.predicate) { if (this.predicate.sameTerm(ns.rdf('type')) && this.superClass) { return utils.label(this.superClass, true) // do initial cap } return utils.label(this.predicate) } else if (this.variable) { return this.variable.toString() } else { return 'unlabeled column?' } } this.setPredicate = function (predicate, inverse, other) { if (inverse) { // variable is in the subject pos this.inverse = predicate this.constraints = this.constraints.concat( kb.each(predicate, UI.ns.rdfs('domain')) ) if ( predicate.sameTerm(ns.rdfs('subClassOf')) && other.termType === 'NamedNode' ) { this.superClass = other this.alternatives = kb.each(undefined, ns.rdfs('subClassOf'), other) } } else { // variable is the object this.predicate = predicate this.constraints = this.constraints.concat( kb.each(predicate, UI.ns.rdfs('range')) ) } } this.getConstraints = function () { return this.constraints } this.filterFunction = function () { return true } this.sortKey = function () { return this.getLabel().toLowerCase() } this.isImageColumn = function () { for (let i = 0; i < this.constraints.length; i++) { if (this.constraints[i].uri in IMAGE_TYPES) return true } return false } } // Convert an object to an array. function objectToArray (obj, filter) { const result = [] for (const property in obj) { // @@@ have to guard against methods const value = obj[property] if (!filter || filter(property, value)) { result.push(value) } } return result } // Generate an