popoto
Version:
Graph based search interface for Neo4j database.
1,469 lines (1,236 loc) • 235 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var jQuery = _interopDefault(require('jquery'));
var d3 = require('d3');
var version = "2.0.8";
var dataModel = {};
dataModel.idGen = 0;
dataModel.generateId = function () {
return dataModel.idGen++;
};
dataModel.nodes = [];
dataModel.links = [];
dataModel.getRootNode = function () {
return dataModel.nodes[0];
};
/**
* logger module.
* @module logger
*/
// LOGGER -----------------------------------------------------------------------------------------------------------
var logger = {};
logger.LogLevels = Object.freeze({DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, NONE: 4});
logger.LEVEL = logger.LogLevels.NONE;
logger.TRACE = false;
/**
* Log a message on console depending on configured log levels.
* Level is define in popoto.logger.LEVEL property.
* If popoto.logger.TRACE is set to true, the stack trace is also added in log.
* @param logLevel Level of the message from popoto.logger.LogLevels.
* @param message Message to log.
*/
logger.log = function (logLevel, message) {
if (console && logLevel >= logger.LEVEL) {
if (logger.TRACE) {
message = message + "\n" + new Error().stack;
}
switch (logLevel) {
case logger.LogLevels.DEBUG:
console.log(message);
break;
case logger.LogLevels.INFO:
console.log(message);
break;
case logger.LogLevels.WARN:
console.warn(message);
break;
case logger.LogLevels.ERROR:
console.error(message);
break;
}
}
};
/**
* Log a message in DEBUG level.
* @param message to log.
*/
logger.debug = function (message) {
logger.log(logger.LogLevels.DEBUG, message);
};
/**
* Log a message in INFO level.
* @param message to log.
*/
logger.info = function (message) {
logger.log(logger.LogLevels.INFO, message);
};
/**
* Log a message in WARN level.
* @param message to log.
*/
logger.warn = function (message) {
logger.log(logger.LogLevels.WARN, message);
};
/**
* Log a message in ERROR level.
* @param message to log.
*/
logger.error = function (message) {
logger.log(logger.LogLevels.ERROR, message);
};
var query = {};
/**
* Define the number of results displayed in result list.
*/
query.MAX_RESULTS_COUNT = 100;
// query.RESULTS_PAGE_NUMBER = 1;
query.VALUE_QUERY_LIMIT = 100;
query.USE_PARENT_RELATION = false;
query.USE_RELATION_DIRECTION = true;
query.COLLECT_RELATIONS_WITH_VALUES = false;
query.prefilter = "";
/**
* Immutable constant object to identify Neo4j internal ID
*/
query.NEO4J_INTERNAL_ID = Object.freeze({queryInternalName: "NEO4JID"});
/**
* Function used to filter returned relations
* return false if the result should be filtered out.
*
* @param d relation returned object
* @returns {boolean}
*/
query.filterRelation = function (d) {
return true;
};
/**
* Generate the query to count nodes of a label.
* If the label is defined as distinct in configuration the query will count only distinct values on constraint attribute.
*/
query.generateTaxonomyCountQuery = function (label) {
var constraintAttr = provider.node.getConstraintAttribute(label);
var whereElements = [];
var predefinedConstraints = provider.node.getPredefinedConstraints(label);
predefinedConstraints.forEach(function (predefinedConstraint) {
whereElements.push(predefinedConstraint.replace(new RegExp("\\$identifier", 'g'), "n"));
});
if (constraintAttr === query.NEO4J_INTERNAL_ID) {
return "MATCH (n:`" + label + "`)" + ((whereElements.length > 0) ? " WHERE " + whereElements.join(" AND ") : "") + " RETURN count(DISTINCT ID(n)) as count"
} else {
return "MATCH (n:`" + label + "`)" + ((whereElements.length > 0) ? " WHERE " + whereElements.join(" AND ") : "") + " RETURN count(DISTINCT n." + constraintAttr + ") as count"
}
};
/**
* Generate Cypher query match and where elements from root node, selected node and a set of the graph links.
*
* @param rootNode root node in the graph.
* @param selectedNode graph target node.
* @param links list of links subset of the graph.
* @returns {{matchElements: Array, whereElements: Array}} list of match and where elements.
* @param isConstraintNeeded (used only for relation query)
* @param useCustomConstraints define whether to use the custom constraints (actually it is used only for results)
*/
query.generateQueryElements = function (rootNode, selectedNode, links, isConstraintNeeded, useCustomConstraints) {
var matchElements = [];
var whereElements = [];
var relationElements = [];
var returnElements = [];
var parameters = {};
var rootPredefinedConstraints = provider.node.getPredefinedConstraints(rootNode.label);
rootPredefinedConstraints.forEach(function (predefinedConstraint) {
whereElements.push(predefinedConstraint.replace(new RegExp("\\$identifier", 'g'), rootNode.internalLabel));
});
matchElements.push("(" + rootNode.internalLabel + ":`" + rootNode.label + "`)");
// Generate root node match element
if (isConstraintNeeded || rootNode.immutable) {
var rootValueConstraints = query.generateNodeValueConstraints(rootNode, useCustomConstraints);
whereElements = whereElements.concat(rootValueConstraints.whereElements);
for (var param in rootValueConstraints.parameters) {
if (rootValueConstraints.parameters.hasOwnProperty(param)) {
parameters[param] = rootValueConstraints.parameters[param];
}
}
}
var relId = 0;
// Generate match elements for each links
links.forEach(function (l) {
var sourceNode = l.source;
var targetNode = l.target;
var rel = "->";
if (!query.USE_RELATION_DIRECTION || targetNode.isParentRelReverse === true) {
rel = "-";
}
var relIdentifier = "r" + relId++;
relationElements.push(relIdentifier);
var predefinedConstraints = provider.node.getPredefinedConstraints(targetNode.label);
predefinedConstraints.forEach(function (predefinedConstraint) {
whereElements.push(predefinedConstraint.replace(new RegExp("\\$identifier", 'g'), targetNode.internalLabel));
});
if (targetNode !== selectedNode && targetNode.isNegative === true) {
if (targetNode.value !== undefined && targetNode.value.length > 0) {
var count = 0;
targetNode.value.forEach(function (v) {
var constraintAttr = provider.node.getConstraintAttribute(v.label);
var param;
if (count === 0) {
param = targetNode.internalLabel + "_" + constraintAttr;
count++;
} else {
param = targetNode.internalLabel + "_" + constraintAttr + count++;
}
whereElements.push("(NOT (" + sourceNode.internalLabel + ":`" + sourceNode.label + "`)-[:`" + l.label + "`]" + rel + "(:`" + targetNode.label + "`{" + constraintAttr + ":$" + param + "}) )");
});
} else {
whereElements.push("(NOT (" + sourceNode.internalLabel + ":`" + sourceNode.label + "`)-[:`" + l.label + "`]" + rel + "(:`" + targetNode.label + "`) )");
}
} else {
if (query.COLLECT_RELATIONS_WITH_VALUES && targetNode === selectedNode) {
returnElements.push("COLLECT(" + relIdentifier + ") AS incomingRels");
}
var sourceLabelStatement = "";
if (!useCustomConstraints || provider.node.getGenerateNodeValueConstraints(sourceNode) === undefined) {
sourceLabelStatement = ":`" + sourceNode.label + "`";
}
var targetLabelStatement = "";
if (!useCustomConstraints || provider.node.getGenerateNodeValueConstraints(targetNode) === undefined) {
targetLabelStatement = ":`" + targetNode.label + "`";
}
matchElements.push("(" + sourceNode.internalLabel + sourceLabelStatement + ")-[" + relIdentifier + ":`" + l.label + "`]" + rel + "(" + targetNode.internalLabel + targetLabelStatement + ")");
}
if (targetNode !== selectedNode && (isConstraintNeeded || targetNode.immutable)) {
var nodeValueConstraints = query.generateNodeValueConstraints(targetNode, useCustomConstraints);
whereElements = whereElements.concat(nodeValueConstraints.whereElements);
for (var param in nodeValueConstraints.parameters) {
if (nodeValueConstraints.parameters.hasOwnProperty(param)) {
parameters[param] = nodeValueConstraints.parameters[param];
}
}
}
});
return {
"matchElements": matchElements,
"whereElements": whereElements,
"relationElements": relationElements,
"returnElements": returnElements,
"parameters": parameters
};
};
/**
* Generate the where and parameter statements for the nodes with value
*
* @param node the node to generate value constraints
* @param useCustomConstraints define whether to use custom generation in popoto config
*/
query.generateNodeValueConstraints = function (node, useCustomConstraints) {
if (useCustomConstraints && provider.node.getGenerateNodeValueConstraints(node) !== undefined) {
return provider.node.getGenerateNodeValueConstraints(node)(node);
} else {
var parameters = {}, whereElements = [];
if (node.value !== undefined && node.value.length > 0) {
var constraintAttr = provider.node.getConstraintAttribute(node.label);
var paramName = node.internalLabel + "_" + constraintAttr;
if (node.value.length > 1) { // Generate IN constraint
if (node.isNegative === undefined || !node.isNegative) {
parameters[paramName] = [];
node.value.forEach(function (value) {
var constraintValue;
if (constraintAttr === query.NEO4J_INTERNAL_ID) {
constraintValue = value.internalID;
} else {
constraintValue = value.attributes[constraintAttr];
}
parameters[paramName].push(constraintValue);
});
if (constraintAttr === query.NEO4J_INTERNAL_ID) {
whereElements.push("ID(" + node.internalLabel + ") IN " + "{`" + paramName + "`}");
} else {
whereElements.push(node.internalLabel + "." + constraintAttr + " IN " + "{`" + paramName + "`}");
}
} else {
var count = 0;
node.value.forEach(function (value) {
var constraintValue;
if (constraintAttr === query.NEO4J_INTERNAL_ID) {
constraintValue = value.internalID;
} else {
constraintValue = value.attributes[constraintAttr];
}
if (count === 0) {
parameters[paramName] = constraintValue;
count++;
} else {
parameters[paramName + count++] = constraintValue;
}
});
}
} else { // Generate = constraint
if (constraintAttr === query.NEO4J_INTERNAL_ID) {
parameters[paramName] = node.value[0].internalID;
} else {
parameters[paramName] = node.value[0].attributes[constraintAttr];
}
if (node.isNegative === undefined || !node.isNegative) {
var operator = "=";
if (constraintAttr === query.NEO4J_INTERNAL_ID) {
whereElements.push("ID(" + node.internalLabel + ") " + operator + " " + "{`" + paramName + "`}");
} else {
whereElements.push(node.internalLabel + "." + constraintAttr + " " + operator + " " + "{`" + paramName + "`}");
}
}
}
}
return {
parameters: parameters,
whereElements: whereElements
}
}
};
/**
* Filter links to get only paths from root to leaf containing a value or being the selectedNode.
* All other paths in the graph containing no value are ignored.
*
* @param rootNode root node of the graph.
* @param targetNode node in the graph target of the query.
* @param initialLinks list of links repreasenting the graph to filter.
* @returns {Array} list of relevant links.
*/
query.getRelevantLinks = function (rootNode, targetNode, initialLinks) {
var links = initialLinks.slice();
var filteredLinks = [];
var finalLinks = [];
// Filter all links to keep only those containing a value or being the selected node.
links.forEach(function (l) {
if ((l.target.value !== undefined && l.target.value.length > 0) || l.target === targetNode || l.target.isNegative) {
filteredLinks.push(l);
}
});
// All the filtered links are removed from initial links list.
filteredLinks.forEach(function (l) {
links.splice(links.indexOf(l), 1);
});
// Then all the intermediate links up to the root node are added to get only the relevant links.
filteredLinks.forEach(function (fl) {
var sourceNode = fl.source;
var search = true;
while (search) {
var intermediateLink = null;
links.forEach(function (l) {
if (l.target === sourceNode) {
intermediateLink = l;
}
});
if (intermediateLink === null) { // no intermediate links needed
search = false;
} else {
if (intermediateLink.source === rootNode) {
finalLinks.push(intermediateLink);
links.splice(links.indexOf(intermediateLink), 1);
search = false;
} else {
finalLinks.push(intermediateLink);
links.splice(links.indexOf(intermediateLink), 1);
sourceNode = intermediateLink.source;
}
}
}
});
return filteredLinks.concat(finalLinks);
};
/**
* Get the list of link defining the complete path from node to root.
* All other links are ignored.
*
* @param node The node where to start in the graph.
* @param links
*/
query.getLinksToRoot = function (node, links) {
var pathLinks = [];
var targetNode = node;
while (targetNode !== dataModel.getRootNode()) {
var nodeLink;
for (var i = 0; i < links.length; i++) {
var link = links[i];
if (link.target === targetNode) {
nodeLink = link;
break;
}
}
if (nodeLink) {
pathLinks.push(nodeLink);
targetNode = nodeLink.source;
}
}
return pathLinks;
};
/**
* Generate a Cypher query to retrieve the results matching the current graph.
*
* @param isGraph
* @returns {{statement: string, parameters: (*|{})}}
*/
query.generateResultQuery = function (isGraph) {
var rootNode = dataModel.getRootNode();
var queryElements = query.generateQueryElements(rootNode, rootNode, query.getRelevantLinks(rootNode, rootNode, dataModel.links), true, true);
var queryMatchElements = queryElements.matchElements,
queryWhereElements = queryElements.whereElements,
queryRelationElements = queryElements.relationElements,
queryReturnElements = [],
queryEndElements = [],
queryParameters = queryElements.parameters;
// Sort results by specified attribute
var resultOrderByAttribute = provider.node.getResultOrderByAttribute(rootNode.label);
if (resultOrderByAttribute) {
var order = provider.node.isResultOrderAscending(rootNode.label) ? "ASC" : "DESC";
queryEndElements.push("ORDER BY " + resultOrderByAttribute + " " + order);
}
queryEndElements.push("LIMIT " + query.MAX_RESULTS_COUNT);
var resultAttributes = provider.node.getReturnAttributes(rootNode.label);
if (!isGraph) {
for (var i = 0; i < resultAttributes.length; i++) {
var attribute = resultAttributes[i];
if (attribute === query.NEO4J_INTERNAL_ID) {
queryReturnElements.push("ID(" + rootNode.internalLabel + ") AS " + query.NEO4J_INTERNAL_ID.queryInternalName);
} else {
queryReturnElements.push(rootNode.internalLabel + "." + attribute + " AS " + attribute);
}
}
} else {
// Only return relations
queryReturnElements.push(rootNode.internalLabel);
queryRelationElements.forEach(
function (el) {
queryReturnElements.push(el);
}
);
}
var queryStatement = "MATCH " + queryMatchElements.join(", ") + ((queryWhereElements.length > 0) ? " WHERE " + queryWhereElements.join(" AND ") : "") + " RETURN DISTINCT " + queryReturnElements.join(", ") + " " + queryEndElements.join(" ");
// Filter the query if defined in config
var queryStructure = provider.node.filterResultQuery(rootNode.label, {
statement: queryStatement,
matchElements: queryMatchElements,
whereElements: queryWhereElements,
withElements: [],
returnElements: queryReturnElements,
endElements: queryEndElements,
parameters: queryParameters
});
queryStructure.statement = query.prefilter + queryStructure.statement;
return queryStructure;
};
/**
* Generate a cypher query to the get the node count, set as parameter matching the current graph.
*
* @param countedNode the counted node
* @returns {string} the node count cypher query
*/
query.generateNodeCountQuery = function (countedNode) {
var queryElements = query.generateQueryElements(dataModel.getRootNode(), countedNode, query.getRelevantLinks(dataModel.getRootNode(), countedNode, dataModel.links), true, true);
var queryMatchElements = queryElements.matchElements,
queryWhereElements = queryElements.whereElements,
queryReturnElements = [],
queryEndElements = [],
queryParameters = queryElements.parameters;
var countAttr = provider.node.getConstraintAttribute(countedNode.label);
if (countAttr === query.NEO4J_INTERNAL_ID) {
queryReturnElements.push("count(DISTINCT ID(" + countedNode.internalLabel + ")) as count");
} else {
queryReturnElements.push("count(DISTINCT " + countedNode.internalLabel + "." + countAttr + ") as count");
}
var queryStatement = "MATCH " + queryMatchElements.join(", ") + ((queryWhereElements.length > 0) ? " WHERE " + queryWhereElements.join(" AND ") : "") + " RETURN " + queryReturnElements.join(", ");
// Filter the query if defined in config
var queryStructure = provider.node.filterNodeCountQuery(countedNode, {
statement: queryStatement,
matchElements: queryMatchElements,
whereElements: queryWhereElements,
returnElements: queryReturnElements,
endElements: queryEndElements,
parameters: queryParameters
});
return {
statement: query.prefilter + queryStructure.statement,
parameters: queryStructure.parameters
};
};
/**
* Generate a Cypher query from the graph model to get all the possible values for the targetNode element.
*
* @param targetNode node in the graph to get the values.
* @returns {string} the query to execute to get all the values of targetNode corresponding to the graph.
*/
query.generateNodeValueQuery = function (targetNode) {
var rootNode = dataModel.getRootNode();
var queryElements = query.generateQueryElements(rootNode, targetNode, query.getRelevantLinks(rootNode, targetNode, dataModel.links), true, false);
var queryMatchElements = queryElements.matchElements,
queryWhereElements = queryElements.whereElements,
queryReturnElements = [],
queryEndElements = [],
queryParameters = queryElements.parameters;
// Sort results by specified attribute
var valueOrderByAttribute = provider.node.getValueOrderByAttribute(targetNode.label);
if (valueOrderByAttribute) {
var order = provider.node.isValueOrderAscending(targetNode.label) ? "ASC" : "DESC";
queryEndElements.push("ORDER BY " + valueOrderByAttribute + " " + order);
}
queryEndElements.push("LIMIT " + query.VALUE_QUERY_LIMIT);
var resultAttributes = provider.node.getReturnAttributes(targetNode.label);
var constraintAttribute = provider.node.getConstraintAttribute(targetNode.label);
for (var i = 0; i < resultAttributes.length; i++) {
if (resultAttributes[i] === query.NEO4J_INTERNAL_ID) {
queryReturnElements.push("ID(" + targetNode.internalLabel + ") AS " + query.NEO4J_INTERNAL_ID.queryInternalName);
} else {
queryReturnElements.push(targetNode.internalLabel + "." + resultAttributes[i] + " AS " + resultAttributes[i]);
}
}
// Add count return attribute on root node
var rootConstraintAttr = provider.node.getConstraintAttribute(rootNode.label);
if (rootConstraintAttr === query.NEO4J_INTERNAL_ID) {
queryReturnElements.push("count(DISTINCT ID(" + rootNode.internalLabel + ")) AS count");
} else {
queryReturnElements.push("count(DISTINCT " + rootNode.internalLabel + "." + rootConstraintAttr + ") AS count");
}
if (query.COLLECT_RELATIONS_WITH_VALUES) {
queryElements.returnElements.forEach(function (re) {
queryReturnElements.push(re);
});
}
var queryStatement = "MATCH " + queryMatchElements.join(", ") + ((queryWhereElements.length > 0) ? " WHERE " + queryWhereElements.join(" AND ") : "") + " RETURN " + queryReturnElements.join(", ") + " " + queryEndElements.join(" ");
// Filter the query if defined in config
var queryStructure = provider.node.filterNodeValueQuery(targetNode, {
statement: queryStatement,
matchElements: queryMatchElements,
whereElements: queryWhereElements,
returnElements: queryReturnElements,
endElements: queryEndElements,
parameters: queryParameters
});
return {
statement: query.prefilter + queryStructure.statement,
parameters: queryStructure.parameters
};
};
/**
* Generate a Cypher query to retrieve all the relation available for a given node.
*
* @param targetNode
* @returns {string}
*/
query.generateNodeRelationQuery = function (targetNode) {
var linksToRoot = query.getLinksToRoot(targetNode, dataModel.links);
var queryElements = query.generateQueryElements(dataModel.getRootNode(), targetNode, linksToRoot, false, false);
var queryMatchElements = queryElements.matchElements,
queryWhereElements = queryElements.whereElements,
queryReturnElements = [],
queryEndElements = [],
queryParameters = queryElements.parameters;
var rel = query.USE_RELATION_DIRECTION ? "->" : "-";
queryMatchElements.push("(" + targetNode.internalLabel + ":`" + targetNode.label + "`)-[r]" + rel + "(x)");
queryReturnElements.push("type(r) AS label");
if (query.USE_PARENT_RELATION) {
queryReturnElements.push("head(labels(x)) AS target");
} else {
queryReturnElements.push("last(labels(x)) AS target");
}
queryReturnElements.push("count(r) AS count");
queryEndElements.push("ORDER BY count(r) DESC");
var queryStatement = "MATCH " + queryMatchElements.join(", ") + ((queryWhereElements.length > 0) ? " WHERE " + queryWhereElements.join(" AND ") : "") + " RETURN " + queryReturnElements.join(", ") + " " + queryEndElements.join(" ");
// Filter the query if defined in config
var queryStructure = provider.node.filterNodeRelationQuery(targetNode, {
statement: queryStatement,
matchElements: queryMatchElements,
whereElements: queryWhereElements,
returnElements: queryReturnElements,
endElements: queryEndElements,
parameters: queryParameters
});
return {
statement: query.prefilter + queryStructure.statement,
parameters: queryStructure.parameters
};
};
/**
* logger module.
* @module logger
*/
var rest = {};
/**
* Default REST URL used to call Neo4j server with cypher queries to execute.
* This property should be updated to access to your own server.
* @type {string}
*/
rest.CYPHER_URL = "http://localhost:7474/db/data/transaction/commit";
rest.WITH_CREDENTIALS = false;
/**
* Create JQuery ajax POST request to access Neo4j REST API.
*
* @param data data object containing Cypher query.
* @param url url where to post the data, default to popoto.rest.CYPHER_URL property.
* @returns {*} the JQuery ajax request object.
*/
rest.post = function (data, url) {
var strData = JSON.stringify(data);
logger.info("REST POST:" + strData);
var settings = {
type: "POST",
beforeSend: function (request) {
if (rest.AUTHORIZATION) {
request.setRequestHeader("Authorization", rest.AUTHORIZATION);
}
},
contentType: "application/json"
};
if (data !== undefined) {
settings.data = strData;
}
if (rest.WITH_CREDENTIALS === true) {
settings.xhrFields = {
withCredentials: true
};
}
var postURL = rest.CYPHER_URL;
if (url !== undefined) {
postURL = url;
}
return jQuery.ajax(postURL, settings);
};
rest.response = {
parse: function (data) {
logger.debug(JSON.stringify((data)));
var parsedData = [];
if (data !== undefined && data.hasOwnProperty("results") && data.results.length > 0 && !(data.hasOwnProperty("errors") && data.errors.length > 0)) {
parsedData = data.results.map(function (queryResults) {
var results = [];
for (var x = 0; x < queryResults.data.length; x++) {
var obj = {};
for (var i = 0; i < queryResults.columns.length; i++) {
obj[queryResults.columns[i]] = queryResults.data[x].row[i];
}
results.push(obj);
}
return results;
});
}
logger.info(JSON.stringify((parsedData)));
return parsedData
}
};
var result = {};
result.containerId = "popoto-results";
result.hasChanged = true;
result.resultCountListeners = [];
result.resultListeners = [];
result.graphResultListeners = [];
result.RESULTS_PAGE_SIZE = 10;
result.TOTAL_COUNT = false;
/**
* Register a listener to the result count event.
* This listener will be called on evry result change with total result count.
*/
result.onTotalResultCount = function (listener) {
result.resultCountListeners.push(listener);
};
result.onResultReceived = function (listener) {
result.resultListeners.push(listener);
};
result.onGraphResultReceived = function (listener) {
result.graphResultListeners.push(listener);
};
/**
* Parse REST returned Graph data and generate a list of nodes and edges.
*
* @param data
* @returns {{nodes: Array, edges: Array}}
*/
result.parseGraphResultData = function (data) {
var nodes = {}, edges = {};
data.results[1].data.forEach(function (row) {
row.graph.nodes.forEach(function (n) {
if (!nodes.hasOwnProperty(n.id)) {
nodes[n.id] = n;
}
});
row.graph.relationships.forEach(function (r) {
if (!edges.hasOwnProperty(r.id)) {
edges[r.id] = r;
}
});
});
var nodesArray = [], edgesArray = [];
for (var n in nodes) {
if (nodes.hasOwnProperty(n)) {
nodesArray.push(nodes[n]);
}
}
for (var e in edges) {
if (edges.hasOwnProperty(e)) {
edgesArray.push(edges[e]);
}
}
return {nodes: nodesArray, edges: edgesArray};
};
result.updateResults = function () {
if (result.hasChanged) {
var resultsIndex = {};
var index = 0;
// Abort any old running request before starting a new one
if (result.resultsXhr !== undefined) {
result.resultsXhr.abort();
}
var resultQuery = query.generateResultQuery();
result.lastGeneratedQuery = resultQuery;
var postData = {
"statements": [
{
"statement": resultQuery.statement,
"parameters": resultQuery.parameters,
"resultDataContents": ["row"]
}
]
};
resultsIndex["results"] = index++;
// Add Graph result query if listener found
if (result.graphResultListeners.length > 0) {
var graphQuery = query.generateResultQuery(true);
result.lastGeneratedQuery = graphQuery;
postData.statements.push(
{
"statement": graphQuery.statement,
"parameters": graphQuery.parameters,
"resultDataContents": ["row", "graph"]
});
resultsIndex["graph"] = index++;
}
if (result.TOTAL_COUNT === true && result.resultCountListeners.length > 0) {
var nodeCountQuery = query.generateNodeCountQuery(dataModel.getRootNode());
postData.statements.push(
{
"statement": nodeCountQuery.statement,
"parameters": nodeCountQuery.parameters
}
);
resultsIndex["total"] = index++;
}
logger.info("Results ==>");
result.resultsXhr = rest.post(postData)
.done(function (response) {
logger.info("<== Results");
var parsedData = rest.response.parse(response);
var resultObjects = parsedData[resultsIndex["results"]].map(function (d, i) {
return {
"resultIndex": i,
"label": dataModel.getRootNode().label,
"attributes": d
};
});
result.lastResults = resultObjects;
if (resultsIndex.hasOwnProperty("total")) {
var count = parsedData[resultsIndex["total"]][0].count;
// Notify listeners
result.resultCountListeners.forEach(function (listener) {
listener(count);
});
}
// Notify listeners
result.resultListeners.forEach(function (listener) {
listener(resultObjects);
});
if (result.graphResultListeners.length > 0) {
var graphResultObjects = result.parseGraphResultData(response);
result.graphResultListeners.forEach(function (listener) {
listener(graphResultObjects);
});
}
// Update displayed results only if needed ()
if (result.isActive) {
// Clear all results
var results = d3.select("#" + result.containerId).selectAll(".ppt-result").data([]);
results.exit().remove();
// Update data
results = d3.select("#" + result.containerId).selectAll(".ppt-result").data(resultObjects.slice(0, result.RESULTS_PAGE_SIZE), function (d) {
return d.resultIndex;
});
// Add new elements
var pElmt = results.enter()
.append("div")
.attr("class", "ppt-result")
.attr("id", function (d) {
return "popoto-result-" + d.resultIndex;
});
// Generate results with providers
pElmt.each(function (d) {
provider.node.getDisplayResults(d.label)(d3.select(this));
});
}
result.hasChanged = false;
})
.fail(function (xhr, textStatus, errorThrown) {
if (textStatus !== "abort") {
logger.error(textStatus + ": error while accessing Neo4j server on URL:\"" + rest.CYPHER_URL + "\" defined in \"rest.CYPHER_URL\" property: " + errorThrown);
// Notify listeners
result.resultListeners.forEach(function (listener) {
listener([]);
});
} else {
logger.info("<=X= Results - Aborted!");
}
});
}
};
result.updateResultsCount = function () {
// Update result counts with root node count
if (result.resultCountListeners.length > 0) {
result.resultCountListeners.forEach(function (listener) {
listener(dataModel.getRootNode().count);
});
}
};
result.generatePreQuery = function () {
var p = {"ids": []};
result.lastResults.forEach(function (d) {
p.ids.push(d.attributes.id);
});
return {
query: "MATCH (d) WHERE d.id IN $ids WITH d",
param: p
};
};
/**
* Main function to call to use Popoto.js.
* This function will create all the HTML content based on available IDs in the page.
*
* @param startParam Root label or graph schema to use in the graph query builder.
*/
//TODO add instance creation + config
function start(startParam) {
logger.info("Popoto " + version + " start on " + startParam);
graph.mainLabel = startParam;
if (rest.CYPHER_URL === undefined) {
logger.error("popoto.rest.CYPHER_URL is not set but is required.");
} else {
// TODO introduce component generator mechanism instead for future plugin extensions
checkHtmlComponents();
if (taxonomy.isActive) {
taxonomy.createTaxonomyPanel();
}
if (graph.isActive) {
graph.createGraphArea();
graph.createForceLayout();
if (typeof startParam === 'string' || startParam instanceof String) {
var labelSchema = provider.node.getSchema(startParam);
if (labelSchema !== undefined) {
graph.addSchema(labelSchema);
} else {
graph.addRootNode(startParam);
}
} else {
graph.loadSchema(startParam);
}
}
if (queryviewer.isActive) {
queryviewer.createQueryArea();
}
if (cypherviewer.isActive) {
cypherviewer.createQueryArea();
}
if (graph.USE_VORONOI_LAYOUT === true) {
graph.voronoi.extent([[-popoto.graph.getSVGWidth(), -popoto.graph.getSVGWidth()], [popoto.graph.getSVGWidth() * 2, popoto.graph.getSVGHeight() * 2]]);
}
update();
}
}
/**
* Check in the HTML page the components to generate.
*/
function checkHtmlComponents() {
var graphHTMLContainer = d3.select("#" + graph.containerId);
var taxonomyHTMLContainer = d3.select("#" + taxonomy.containerId);
var queryHTMLContainer = d3.select("#" + queryviewer.containerId);
var cypherHTMLContainer = d3.select("#" + cypherviewer.containerId);
var resultsHTMLContainer = d3.select("#" + result.containerId);
if (graphHTMLContainer.empty()) {
logger.debug("The page doesn't contain a container with ID = \"" + graph.containerId + "\" no graph area will be generated. This ID is defined in graph.containerId property.");
graph.isActive = false;
} else {
graph.isActive = true;
}
if (taxonomyHTMLContainer.empty()) {
logger.debug("The page doesn't contain a container with ID = \"" + taxonomy.containerId + "\" no taxonomy filter will be generated. This ID is defined in taxonomy.containerId property.");
taxonomy.isActive = false;
} else {
taxonomy.isActive = true;
}
if (queryHTMLContainer.empty()) {
logger.debug("The page doesn't contain a container with ID = \"" + queryviewer.containerId + "\" no query viewer will be generated. This ID is defined in queryviewer.containerId property.");
queryviewer.isActive = false;
} else {
queryviewer.isActive = true;
}
if (cypherHTMLContainer.empty()) {
logger.debug("The page doesn't contain a container with ID = \"" + cypherviewer.containerId + "\" no cypher query viewer will be generated. This ID is defined in cypherviewer.containerId property.");
cypherviewer.isActive = false;
} else {
cypherviewer.isActive = true;
}
if (resultsHTMLContainer.empty()) {
logger.debug("The page doesn't contain a container with ID = \"" + result.containerId + "\" no result area will be generated. This ID is defined in result.containerId property.");
result.isActive = false;
} else {
result.isActive = true;
}
}
/**
* Function to call to update all the generated elements including svg graph, query viewer and generated results.
*/
function update() {
updateGraph();
// Do not update if rootNode is not valid.
var root = dataModel.getRootNode();
if (!root || root.label === undefined) {
return;
}
if (queryviewer.isActive) {
queryviewer.updateQuery();
}
if (cypherviewer.isActive) {
cypherviewer.updateQuery();
}
// Results are updated only if needed.
// If id found in html page or if result listeners have been added.
// In this case the query must be executed.
if (result.isActive || result.resultListeners.length > 0 || result.resultCountListeners.length > 0 || result.graphResultListeners.length > 0) {
result.updateResults();
}
}
/**
* Function to call to update the graph only.
*/
function updateGraph() {
if (graph.isActive) {
// Starts the D3.js force simulation.
// This method must be called when the layout is first created, after assigning the nodes and links.
// In addition, it should be called again whenever the nodes or links change.
graph.link.updateLinks();
graph.node.updateNodes();
// Force simulation restart
graph.force.nodes(dataModel.nodes);
graph.force.force("link").links(dataModel.links);
graph.force.alpha(1).restart();
}
}
var taxonomy = {};
taxonomy.containerId = "popoto-taxonomy";
/**
* Create the taxonomy panel HTML elements.
*/
taxonomy.createTaxonomyPanel = function () {
var htmlContainer = d3.select("#" + taxonomy.containerId);
var taxoUL = htmlContainer.append("ul")
.attr("class", "ppt-taxo-ul");
var data = taxonomy.generateTaxonomiesData();
var taxos = taxoUL.selectAll(".taxo").data(data);
var taxoli = taxos.enter().append("li")
.attr("id", function (d) {
return d.id
})
.attr("class", "ppt-taxo-li")
.attr("value", function (d) {
return d.label;
});
taxoli.append("span")
.attr("class", function (d) {
return "ppt-icon " + provider.taxonomy.getCSSClass(d.label, "span-icon");
})
.html(" ");
taxoli.append("span")
.attr("class", "ppt-label")
.text(function (d) {
return provider.taxonomy.getTextValue(d.label);
});
taxoli.append("span")
.attr("class", "ppt-count");
// Add an on click event on the taxonomy to clear the graph and set this label as root
taxoli.on("click", taxonomy.onClick);
taxonomy.addTaxonomyChildren(taxoli);
// The count is updated for each labels.
var flattenData = [];
data.forEach(function (d) {
flattenData.push(d);
if (d.children) {
taxonomy.flattenChildren(d, flattenData);
}
});
if (!graph.DISABLE_COUNT) {
taxonomy.updateCount(flattenData);
}
};
/**
* Recursive function to flatten data content.
*
*/
taxonomy.flattenChildren = function (d, vals) {
d.children.forEach(function (c) {
vals.push(c);
if (c.children) {
vals.concat(taxonomy.flattenChildren(c, vals));
}
});
};
/**
* Updates the count number on a taxonomy.
*
* @param taxonomyData
*/
taxonomy.updateCount = function (taxonomyData) {
var statements = [];
taxonomyData.forEach(function (taxo) {
statements.push(
{
"statement": query.generateTaxonomyCountQuery(taxo.label)
}
);
});
(function (taxonomies) {
logger.info("Count taxonomies ==>");
rest.post(
{
"statements": statements
})
.done(function (response) {
logger.info("<== Count taxonomies");
var parsedData = rest.response.parse(response);
for (var i = 0; i < taxonomies.length; i++) {
var count = parsedData[i][0].count;
d3.select("#" + taxonomies[i].id)
.select(".ppt-count")
.text(" (" + count + ")");
}
})
.fail(function (xhr, textStatus, errorThrown) {
logger.error(textStatus + ": error while accessing Neo4j server on URL:\"" + rest.CYPHER_URL + "\" defined in \"rest.CYPHER_URL\" property: " + errorThrown);
d3.select("#popoto-taxonomy")
.selectAll(".ppt-count")
.text(" (0)");
});
})(taxonomyData);
};
/**
* Recursively generate the taxonomy children elements.
*
* @param selection
*/
taxonomy.addTaxonomyChildren = function (selection) {
selection.each(function (d) {
var li = d3.select(this);
var children = d.children;
if (d.children) {
var childLi = li.append("ul")
.attr("class", "ppt-taxo-sub-ul")
.selectAll("li")
.data(children)
.enter()
.append("li")
.attr("id", function (d) {
return d.id
})
.attr("class", "ppt-taxo-sub-li")
.attr("value", function (d) {
return d.label;
});
childLi.append("span")
.attr("class", function (d) {
return "ppt-icon " + provider.taxonomy.getCSSClass(d.label, "span-icon");
})
.html(" ");
childLi.append("span")
.attr("class", "ppt-label")
.text(function (d) {
return provider.taxonomy.getTextValue(d.label);
});
childLi.append("span")
.attr("class", "ppt-count");
childLi.on("click", taxonomy.onClick);
taxonomy.addTaxonomyChildren(childLi);
}
});
};
taxonomy.onClick = function () {
d3.event.stopPropagation();
var label = this.attributes.value.value;
dataModel.nodes.length = 0;
dataModel.links.length = 0;
// Reinitialize internal label generator
graph.node.internalLabels = {};
update();
graph.mainLabel = label;
if (provider.node.getSchema(label) !== undefined) {
graph.addSchema(provider.node.getSchema(label));
} else {
graph.addRootNode(label);
}
graph.hasGraphChanged = true;
result.hasChanged = true;
graph.ignoreCount = false;
update();
tools.center();
};
/**
* Parse the list of label providers and return a list of data object containing only searchable labels.
* @returns {Array}
*/
taxonomy.generateTaxonomiesData = function () {
var id = 0;
var data = [];
// Retrieve root providers (searchable and without parent)
for (var label in provider.node.Provider) {
if (provider.node.Provider.hasOwnProperty(label)) {
if (provider.node.getProperty(label, "isSearchable") && !provider.node.Provider[label].parent) {
data.push({
"label": label,
"id": "popoto-lbl-" + id++
});
}
}
}
// Add children data for each provider with children.
data.forEach(function (d) {
if (provider.node.getProvider(d.label).hasOwnProperty("children")) {
id = taxonomy.addChildrenData(d, id);
}
});
return data;
};
/**
* Add children providers data.
* @param parentData
* @param id
*/
taxonomy.addChildrenData = function (parentData, id) {
parentData.children = [];
provider.node.getProvider(parentData.label).children.forEach(function (d) {
var childProvider = provider.node.getProvider(d);
var childData = {
"label": d,
"id": "popoto-lbl-" + id++
};
if (childProvider.hasOwnProperty("children")) {
id = taxonomy.addChildrenData(childData, id);
}
if (provider.node.getProperty(d, "isSearchable")) {
parentData.children.push(childData);
}
});
return id;
};
// TOOLS -----------------------------------------------------------------------------------------------------------
var tools = {};
// TODO introduce plugin mechanism to add tools
tools.CENTER_GRAPH = true;
tools.RESET_GRAPH = true;
tools.SAVE_GRAPH = false;
tools.TOGGLE_TAXONOMY = false;
tools.TOGGLE_FULL_SCREEN = true;
tools.TOGGLE_VIEW_RELATION = true;
tools.TOGGLE_FIT_TEXT = true;
/**
* Reset the graph to display the root node only.
*/
tools.reset = function () {
dataModel.nodes.length = 0;
dataModel.links.length = 0;
// Reinitialize internal label generator
graph.node.internalLabels = {};
if (typeof graph.mainLabel === 'string' || graph.mainLabel instanceof String) {
if (provider.node.getSchema(graph.mainLabel) !== undefined) {
graph.addSchema(provider.node.getSchema(graph.mainLabel));
} else {
graph.addRootNode(graph.mainLabel);
}
} else {
graph.loadSchema(graph.mainLabel);
}
graph.hasGraphChanged = true;
result.hasChanged = true;
update();
tools.center();
};
/**
* Reset zoom and center the view on svg center.
*/
tools.center = function () {
graph.svgTag.transition().call(graph.zoom.transform, d3.zoomIdentity);
};
/**
* Show, hide taxonomy panel.
*/
tools.toggleTaxonomy = function () {
var taxo = d3.select("#" + taxonomy.containerId);
if (taxo.filter(".disabled").empty()) {
taxo.classed("disabled", true);
} else {
taxo.classed("disabled", false);
}
graph.centerRootNode();
};
/**
* Enable, disable text fitting on nodes.
*/
tools.toggleFitText = function () {
graph.USE_FIT_TEXT = !graph.USE_FIT_TEXT;
graph.node.updateNodes();
};
/**
* Show, hide relation donuts.
*/
tools.toggleViewRelation = function () {
graph.DISABLE_RELATION = !graph.DISABLE_RELATION;
d3.selectAll(".ppt-g-node-background").classed("hide", graph.DISABLE_RELATION);
graph.tick();
};
tools.toggleFullScreen = function () {
var elem = document.getElementById(graph.containerId);
if (!document.fullscreenElement && // alternative standard method
!document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { // current working methods
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.msRequestFullscreen) {
elem.msRequestFullscreen();
} else if (elem.mozRequestFullScreen) {
elem.mozRequestFullScreen();
} else if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
};
var toolbar = {};
toolbar.TOOL_TAXONOMY = "Show/hide taxonomy panel";
toolbar.TOOL_RELATION = "Show/hide relation";
toolbar.TOOL_CENTER = "Center view";
toolbar.TOOL_FULL_SCREEN = "Full screen";
toolbar.TOOL_RESET = "Reset graph";
toolbar.TOOL_SAVE = "Save graph";
toolbar.TOOL_FIT_TEXT = "Fit text in nodes";
toolbar.render = function (container) {
var toolbar = container
.append("div")
.attr("class", "ppt-toolbar");
if (tools.TOGGLE_VIEW_RELATION) {
toolbar.append("span")
.attr("id", "popoto-toggle-relation")
.attr("class", "ppt-icon ppt-menu relation")
.attr("title", toolbar.TOOL_RELATION)
.on("click", function () {
tools.toggleViewRelation();
});
}
if (tools.RESET_GRAPH) {
too