UNPKG

popoto

Version:

Graph based search interface for Neo4j database.

7,117 lines 240 kB
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var d3 = require('d3');

function _interopNamespace(e) {
    if (e && e.__esModule) return e;
    var n = Object.create(null);
    if (e) {
        Object.keys(e).forEach(function (k) {
            if (k !== 'default') {
                var d = Object.getOwnPropertyDescriptor(e, k);
                Object.defineProperty(n, k, d.get ? d : {
                    enumerable: true,
                    get: function () {
                        return e[k];
                    }
                });
            }
        });
    }
    n['default'] = e;
    return Object.freeze(n);
}

var d3__namespace = /*#__PURE__*/_interopNamespace(d3);

var version = "3.0.0-RC2";

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.RETURN_LABELS = false;
query.COLLECT_RELATIONS_WITH_VALUES = false;
query.prefilter = "";
query.prefilterParameters = {};

query.applyPrefilters = function (queryStructure) {
    queryStructure.statement = query.prefilter + queryStructure.statement;

    Object.keys(query.prefilterParameters).forEach(function (key) {
        queryStructure.parameters[key] = query.prefilterParameters[key];
    });

    return queryStructure;
};

/**
 * 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$1.node.getConstraintAttribute(label);

    var whereElements = [];

    var predefinedConstraints = provider$1.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"
    }
};

query.generateNegativeQueryElements = function () {
    var whereElements = [];
    var parameters = {};

    var negativeNodes = dataModel.nodes.filter(function (n) {
        return n.isNegative === true;
    });

    negativeNodes.forEach(
        function (n) {
            if (provider$1.node.getGenerateNegativeNodeValueConstraints(n) !== undefined) {
                var custom = provider$1.node.getGenerateNegativeNodeValueConstraints(n)(n);
                whereElements = whereElements.concat(custom.whereElements);
                for (var prop in custom.parameters) {
                    if (custom.parameters.hasOwnProperty(prop)) {
                        parameters[prop] = custom.parameters[prop];
                    }
                }
            } else {
                var linksToRoot = query.getLinksToRoot(n, dataModel.links);

                var i = linksToRoot.length - 1;
                var statement = "(NOT exists(";

                statement += "(" + dataModel.getRootNode().internalLabel + ")";

                while (i >= 0) {
                    var l = linksToRoot[i];
                    var targetNode = l.target;

                    if (targetNode.isParentRelReverse === true && query.USE_RELATION_DIRECTION === true) {
                        statement += "<-";
                    } else {
                        statement += "-";
                    }

                    statement += "[:`" + l.label + "`]";

                    if (targetNode.isParentRelReverse !== true && query.USE_RELATION_DIRECTION === true) {
                        statement += "->";
                    } else {
                        statement += "-";
                    }

                    if (targetNode === n && targetNode.value !== undefined && targetNode.value.length > 0) {
                        var constraintAttr = provider$1.node.getConstraintAttribute(targetNode.label);
                        var paramName = targetNode.internalLabel + "_" + constraintAttr;

                        if (targetNode.value.length > 1) {
                            for (var pid = 0; pid < targetNode.value.length; pid++) {
                                parameters[paramName + "_" + pid] = targetNode.value[pid].attributes[constraintAttr];
                            }

                            statement += "(:`" + targetNode.label + "`{" + constraintAttr + ":$x$})";
                        } else {
                            parameters[paramName] = targetNode.value[0].attributes[constraintAttr];
                            statement += "(:`" + targetNode.label + "`{" + constraintAttr + ":$" + paramName + "})";
                        }
                    } else {
                        statement += "(:`" + targetNode.label + "`)";
                    }

                    i--;
                }

                statement += "))";

                if (n.value !== undefined && n.value.length > 1) {
                    var cAttr = provider$1.node.getConstraintAttribute(n.label);
                    var pn = n.internalLabel + "_" + cAttr;

                    for (var nid = 0; nid < targetNode.value.length; nid++) {
                        whereElements.push(statement.replace("$x$", "$" + pn + "_" + nid));
                    }
                } else {
                    whereElements.push(statement);
                }
            }
        }
    );

    return {
        "whereElements": whereElements,
        "parameters": parameters
    };
};

/**
 * 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$1.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 sourceRel = "";
        var targetRel = "";

        if (!query.USE_RELATION_DIRECTION) {
            sourceRel = "-";
            targetRel = "-";
        } else {
            if (targetNode.isParentRelReverse === true) {
                sourceRel = "<-";
                targetRel = "-";
            } else {
                sourceRel = "-";
                targetRel = "->";
            }
        }

        var relIdentifier = "r" + relId++;
        relationElements.push(relIdentifier);
        var predefinedConstraints = provider$1.node.getPredefinedConstraints(targetNode.label);

        predefinedConstraints.forEach(function (predefinedConstraint) {
            whereElements.push(predefinedConstraint.replace(new RegExp("\\$identifier", 'g'), targetNode.internalLabel));
        });

        if (query.COLLECT_RELATIONS_WITH_VALUES && targetNode === selectedNode) {
            returnElements.push("COLLECT(" + relIdentifier + ") AS incomingRels");
        }

        var sourceLabelStatement = "";

        if (!useCustomConstraints || provider$1.node.getGenerateNodeValueConstraints(sourceNode) === undefined) {
            sourceLabelStatement = ":`" + sourceNode.label + "`";
        }

        var targetLabelStatement = "";

        if (!useCustomConstraints || provider$1.node.getGenerateNodeValueConstraints(targetNode) === undefined) {
            targetLabelStatement = ":`" + targetNode.label + "`";
        }

        matchElements.push("(" + sourceNode.internalLabel + sourceLabelStatement + ")" + sourceRel + "[" + relIdentifier + ":`" + l.label + "`]" + targetRel + "(" + 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$1.node.getGenerateNodeValueConstraints(node) !== undefined) {
        return provider$1.node.getGenerateNodeValueConstraints(node)(node);
    } else {
        var parameters = {}, whereElements = [];
        if (node.value !== undefined && node.value.length > 0) {
            var constraintAttr = provider$1.node.getConstraintAttribute(node.label);
            var paramName;
            if (constraintAttr === query.NEO4J_INTERNAL_ID) {
                paramName = node.internalLabel + "_internalID";
            } else {
                paramName = node.internalLabel + "_" + constraintAttr;
            }

            if (node.value.length > 1) { // Generate IN constraint
                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 { // Generate = constraint
                if (constraintAttr === query.NEO4J_INTERNAL_ID) {
                    parameters[paramName] = node.value[0].internalID;
                } else {
                    parameters[paramName] = node.value[0].attributes[constraintAttr];
                }

                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 representing the graph to filter.
 * @returns {Array} list of relevant links.
 */
query.getRelevantLinks = function (rootNode, targetNode, initialLinks) {
    var links = initialLinks.slice();
    var finalLinks = [];

    // Filter all links to keep only those containing a value or being the selected node.
    // Negatives nodes are handled separately.
    var filteredLinks = links.filter(function (l) {
        return l.target === targetNode || ((l.target.value !== undefined && l.target.value.length > 0) && (!l.target.isNegative === true));
    });

    // 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 negativeElements = query.generateNegativeQueryElements();
    var queryElements = query.generateQueryElements(rootNode, rootNode, query.getRelevantLinks(rootNode, rootNode, dataModel.links), true, true);
    var queryMatchElements = queryElements.matchElements,
        queryWhereElements = queryElements.whereElements.concat(negativeElements.whereElements),
        queryRelationElements = queryElements.relationElements,
        queryReturnElements = [],
        queryEndElements = [],
        queryParameters = queryElements.parameters;

    for (var prop in negativeElements.parameters) {
        if (negativeElements.parameters.hasOwnProperty(prop)) {
            queryParameters[prop] = negativeElements.parameters[prop];
        }
    }

    // Sort results by specified attribute
    var resultOrderByAttribute = provider$1.node.getResultOrderByAttribute(rootNode.label);

    if (resultOrderByAttribute !== undefined && resultOrderByAttribute !== null) {
        var sorts = [];
        var order = provider$1.node.isResultOrderAscending(rootNode.label);

        var orders = [];
        if (Array.isArray(order)) {
            orders = order.map(function (v) {
                return v ? "ASC" : "DESC";
            });
        } else {
            orders.push(order ? "ASC" : "DESC");
        }

        if (Array.isArray(resultOrderByAttribute)) {
            sorts = resultOrderByAttribute.map(function (ra) {
                var index = resultOrderByAttribute.indexOf(ra);

                if (index < orders.length) {
                    return ra + " " + orders[index];
                } else {
                    return ra + " " + orders[orders.length - 1];
                }
            });

        } else {
            sorts.push(resultOrderByAttribute + " " + orders[0]);
        }

        queryEndElements.push("ORDER BY " + sorts.join(", "));
    }

    queryEndElements.push("LIMIT " + query.MAX_RESULTS_COUNT);

    if (isGraph) {
        // Only return relations
        queryReturnElements.push(rootNode.internalLabel);
        queryRelationElements.forEach(
            function (el) {
                queryReturnElements.push(el);
            }
        );
    } else {
        var resultAttributes = provider$1.node.getReturnAttributes(rootNode.label);

        queryReturnElements = resultAttributes.map(function (attribute) {
            if (attribute === query.NEO4J_INTERNAL_ID) {
                return "ID(" + rootNode.internalLabel + ") AS " + query.NEO4J_INTERNAL_ID.queryInternalName;
            } else {
                return rootNode.internalLabel + "." + attribute + " AS " + attribute;
            }
        });

        if (query.RETURN_LABELS === true) {
            var element = "labels(" + rootNode.internalLabel + ")";

            if (resultAttributes.indexOf("labels") < 0) {
                element = element + " AS labels";
            }

            queryReturnElements.push(element);
        }
    }

    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$1.node.filterResultQuery(rootNode.label, {
        statement: queryStatement,
        matchElements: queryMatchElements,
        whereElements: queryWhereElements,
        withElements: [],
        returnElements: queryReturnElements,
        endElements: queryEndElements,
        parameters: queryParameters
    });

    return query.applyPrefilters(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 negativeElements = query.generateNegativeQueryElements();
    var queryElements = query.generateQueryElements(dataModel.getRootNode(), countedNode, query.getRelevantLinks(dataModel.getRootNode(), countedNode, dataModel.links), true, true);
    var queryMatchElements = queryElements.matchElements,
        queryWhereElements = queryElements.whereElements.concat(negativeElements.whereElements),
        queryReturnElements = [],
        queryEndElements = [],
        queryParameters = queryElements.parameters;

    for (var prop in negativeElements.parameters) {
        if (negativeElements.parameters.hasOwnProperty(prop)) {
            queryParameters[prop] = negativeElements.parameters[prop];
        }
    }

    var countAttr = provider$1.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$1.node.filterNodeCountQuery(countedNode, {
        statement: queryStatement,
        matchElements: queryMatchElements,
        whereElements: queryWhereElements,
        returnElements: queryReturnElements,
        endElements: queryEndElements,
        parameters: queryParameters
    });

    return query.applyPrefilters(queryStructure);
};

/**
 * 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 negativeElements = query.generateNegativeQueryElements();
    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.concat(negativeElements.whereElements),
        queryReturnElements = [],
        queryEndElements = [],
        queryParameters = queryElements.parameters;

    for (var prop in negativeElements.parameters) {
        if (negativeElements.parameters.hasOwnProperty(prop)) {
            queryParameters[prop] = negativeElements.parameters[prop];
        }
    }

    // Sort results by specified attribute
    var valueOrderByAttribute = provider$1.node.getValueOrderByAttribute(targetNode.label);
    if (valueOrderByAttribute) {
        var order = provider$1.node.isValueOrderAscending(targetNode.label) ? "ASC" : "DESC";
        queryEndElements.push("ORDER BY " + valueOrderByAttribute + " " + order);
    }

    queryEndElements.push("LIMIT " + query.VALUE_QUERY_LIMIT);

    var resultAttributes = provider$1.node.getReturnAttributes(targetNode.label);
    provider$1.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$1.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$1.node.filterNodeValueQuery(targetNode, {
        statement: queryStatement,
        matchElements: queryMatchElements,
        whereElements: queryWhereElements,
        returnElements: queryReturnElements,
        endElements: queryEndElements,
        parameters: queryParameters
    });

    return query.applyPrefilters(queryStructure);
};

/**
 * 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$1.node.filterNodeRelationQuery(targetNode, {
        statement: queryStatement,
        matchElements: queryMatchElements,
        whereElements: queryWhereElements,
        returnElements: queryReturnElements,
        endElements: queryEndElements,
        parameters: queryParameters
    });

    return query.applyPrefilters(queryStructure);
};

/**
 * runner module.
 * @module runner
 */

var runner = {};

runner.createSession = function () {
    if (runner.DRIVER !== undefined) {
        return runner.DRIVER.session({defaultAccessMode: "READ"})
    } else {
        throw new Error("popoto.runner.DRIVER must be defined");
    }
};

runner.run = function (statements) {
    logger.info("STATEMENTS:" + JSON.stringify(statements));
    var session = runner.createSession();

    return session.readTransaction(function (transaction) {
        return Promise.all(
            statements.statements.map(function (s) {
                return transaction.run({text: s.statement, parameters: s.parameters});
            })
        )
    })
        .finally(function () {
            session.close();
        })
};

runner.toObject = function (results) {
    return results.map(function (rs) {
        return rs.records.map(function (r) {
            return r.toObject();
        })
    })
};

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;

        var resultQuery = query.generateResultQuery();
        result.lastGeneratedQuery = resultQuery;

        var postData = {
            "statements": [
                {
                    "statement": resultQuery.statement,
                    "parameters": resultQuery.parameters,
                }
            ]
        };
        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,
                });
            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 ==>");

        runner.run(postData)
            .then(function (res) {
                logger.info("<== Results");

                var parsedData = runner.toObject(res);

                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__namespace.select("#" + result.containerId).selectAll(".ppt-result").data([]);
                    results.exit().remove();
                    // Update data
                    results = d3__namespace.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$1.node.getDisplayResults(d.label)(d3__namespace.select(this));
                    });
                }

                result.hasChanged = false;
            })
            .catch(function (error) {
                logger.error(error);

                // Notify listeners
                result.resultListeners.forEach(function (listener) {
                    listener([]);
                });
            });
    }
};

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.
 */
function start(startParam) {
    logger.info("Popoto " + version + " start on " + startParam);

    graph$1.mainLabel = startParam;

    checkHtmlComponents();

    if (taxonomy.isActive) {
        taxonomy.createTaxonomyPanel();
    }

    if (graph$1.isActive) {
        graph$1.createGraphArea();
        graph$1.createForceLayout();

        if (typeof startParam === 'string' || startParam instanceof String) {
            var labelSchema = provider$1.node.getSchema(startParam);
            if (labelSchema !== undefined) {
                graph$1.addSchema(labelSchema);
            } else {
                graph$1.addRootNode(startParam);
            }
        } else {
            graph$1.loadSchema(startParam);
        }
    }

    if (queryviewer.isActive) {
        queryviewer.createQueryArea();
    }

    if (cypherviewer.isActive) {
        cypherviewer.createQueryArea();
    }

    if (graph$1.USE_VORONOI_LAYOUT === true) {
        graph$1.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__namespace.select("#" + graph$1.containerId);
    var taxonomyHTMLContainer = d3__namespace.select("#" + taxonomy.containerId);
    var queryHTMLContainer = d3__namespace.select("#" + queryviewer.containerId);
    var cypherHTMLContainer = d3__namespace.select("#" + cypherviewer.containerId);
    var resultsHTMLContainer = d3__namespace.select("#" + result.containerId);

    if (graphHTMLContainer.empty()) {
        logger.debug("The page doesn't contain a container with ID = \"" + graph$1.containerId + "\" no graph area will be generated. This ID is defined in graph.containerId property.");
        graph$1.isActive = false;
    } else {
        graph$1.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$1.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$1.link.updateLinks();
        graph$1.node.updateNodes();

        // Force simulation restart
        graph$1.force.nodes(dataModel.nodes);
        graph$1.force.force("link").links(dataModel.links);
        graph$1.force.alpha(1).restart();
    }
}

var taxonomy = {};
taxonomy.containerId = "popoto-taxonomy";

/**
 * Create the taxonomy panel HTML elements.
 */
taxonomy.createTaxonomyPanel = function () {
    var htmlContainer = d3__namespace.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$1.taxonomy.getCSSClass(d.label, "span-icon");
        })
        .html("&nbsp;");

    taxoli.append("span")
        .attr("class", "ppt-label")
        .text(function (d) {
            return provider$1.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$1.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 ==>");
        runner.run(
            {
                "statements": statements
            })
            .then(function (results) {
                logger.info("<== Count taxonomies");

                for (var i = 0; i < taxonomies.length; i++) {
                    var count = results[i].records[0].get('count').toString();
                    d3__namespace.select("#" + taxonomies[i].id)
                        .select(".ppt-count")
                        .text(" (" + count + ")");
                }
            }, function (error) {
                logger.error(error);
                d3__namespace.select("#popoto-taxonomy")
                    .selectAll(".ppt-count")
                    .text(" (0)");
            })
            .catch(function (error) {
                logger.error(error);
                d3__namespace.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__namespace.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$1.taxonomy.getCSSClass(d.label, "span-icon");
                })
                .html("&nbsp;");

            childLi.append("span")
                .attr("class", "ppt-label")
                .text(function (d) {
                    return provider$1.taxonomy.getTextValue(d.label);
                });

            childLi.append("span")
                .attr("class", "ppt-count");

            childLi.on("click", taxonomy.onClick);

            taxonomy.addTaxonomyChildren(childLi);
        }

    });
};

taxonomy.onClick = function () {
    d3__namespace.event.stopPropagation();

    var label = this.attributes.value.value;

    dataModel.nodes.length = 0;
    dataModel.links.length = 0;

    // Reinitialize internal label generator
    graph$1.node.internalLabels = {};

    update();
    graph$1.mainLabel = label;
    if (provider$1.node.getSchema(label) !== undefined) {
        graph$1.addSchema(provider$1.node.getSchema(label));
    } else {
        graph$1.addRootNode(label);
    }
    graph$1.hasGraphChanged = true;
    result.hasChanged = true;
    graph$1.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$1.node.Provider) {
        if (provider$1.node.Provider.hasOwnProperty(label)) {
            if (provider$1.node.getProperty(label, "isSearchable") && !provider$1.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$1.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$1.node.getProvider(parentData.label).children.forEach(function (d) {
        var childProvider = provider$1.node.getProvider(d);
        var childData = {
            "label": d,
            "id": "popoto-lbl-" + id++
        };
        if (childProvider.hasOwnProperty("children")) {
            id = taxonomy.addChildrenData(childData, id);
        }
        if (provider$1.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$1.node.internalLabels = {};

    if (typeof graph$1.mainLabel === 'string' || graph$1.mainLabel instanceof String) {
        if (provider$1.node.getSchema(graph$1.mainLabel) !== undefined) {
            graph$1.addSchema(provider$1.node.getSchema(graph$1.mainLabel));
        } else {
            graph$1.addRootNode(graph$1.mainLabel);
        }
    } else {
        graph$1.loadSchema(graph$1.mainLabel);
    }

    graph$1.hasGraphChanged = true;
    result.hasChanged = true;
    update();
    tools.center();
};

/**
 * Reset zoom and center the view on svg center.
 */
tools.center = function () {
    graph$1.svgTag.transition().call(graph$1.zoom.transform, d3__namespace.zoomIdentity);
};

/**
 * Show, hide taxonomy panel.
 */
tools.toggleTaxonomy = function () {
    var taxo = d3__namespace.select("#" + taxonomy.containerId);
    if (taxo.filter(".disabled").empty()) {
        taxo.classed("disabled", true);
    } else {
        taxo.classed("disabled", false);
    }

    graph$1.centerRootNode();
};

/**
 * Enable, disable text fitting on nodes.
 */
tools.toggleFitText = function () {
    graph$1.USE_FIT_TEXT = !graph$1.USE_FIT_TEXT;
    graph$1.node.updateNodes();
};

/**
 * Show, hide relation donuts.
 */
tools.toggleViewRelation = function () {
    graph$1.DISABLE_RELATION = !graph$1.DISABLE_RELATION;
    d3__namespace.selectAll(".ppt-g-node-background").classed("hide", graph$1.DISABLE_RELATION);
    graph$1.tick();
};

tools.toggleFullScreen = function () {

    var elem = document.getElementById(graph$1.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) {
        toolbar.append("span")
            .attr("id", "popoto-reset-menu")
            .attr("class", "ppt-icon ppt-menu reset")
            .attr("title", toolbar.TOOL_RESET)
            .on("click", function () {
                graph$1.notifyListeners(graph$1.Events.GRAPH_RESET, []);
                tools.reset();
            });
    }

    if (tools.TOGGLE_TAXONOMY) {
        toolbar.append("span")
            .attr("id", "popoto-taxonomy-menu")
            .attr("class", "ppt-icon ppt-menu taxonomy")
            .attr("title", toolbar.TOOL_TAXONOMY)
            .on("click", tools.toggleTaxonomy);
    }

    if (tools.CENTER_GRAPH) {
        toolbar.append("span")
            .attr("id", "popoto-center-menu")
            .attr("class", "ppt-icon ppt-menu center")
            .attr("title", toolbar.TOOL_CENTER)
            .on("click", tools.center);
    }

    if (tools.TOGGLE_FULL_SCREEN) {
        toolbar.append("span")
            .attr("id", "popoto-fullscreen-menu")
            .attr("class", "ppt-icon ppt-menu fullscreen")
            .attr("title", toolbar.TOOL_FULL_SCREEN)
            .on("click", tools.toggleFullScreen);
    }

    if (tools.SAVE_GRAPH) {
        toolbar.append("span")
            .attr("id", "popoto-save-menu")
            .attr("class", "ppt-icon ppt-menu save")
            .attr("title", toolbar.TOOL_SAVE)
            .on("click", function () {
                graph$1.notifyListeners(graph$1.Events.GRAPH_SAVE, [graph$1.getSchema()]);
            });
    }

    if (tools.TOGGLE_FIT_TEXT) {
        toolbar.append("span")
            .attr("id", "popoto-fit-text-menu")
            .attr("class", "ppt-icon ppt-menu fit-text")
            .attr("title", toolbar.TOOL_FIT_TEXT)
            .on("click", tools.toggleFitText);
    }
};

var link = {};

/**
 * Defines the different type of link.
 * RELATION is a relation link between two nodes.
 * VALUE is a link between a generic node and a value.
 */
link.LinkTypes = Object.freeze({RELATION: 0, VALUE: 1, SEGMENT: 2});

/**
 * Offset added to text displayed on links.
 * @type {number}
 */
link.TEXT_DY = -4;

/**
 * Define whether or not to display path markers.
 */
link.SHOW_MARKER = false;

// ID of the g element in SVG graph containing all the link elements.
link.gID = "popoto-glinks";

/**
 * Function to call to update the links after modification in the model.
 * This function will update the graph with all removed, modified or added links using d3.js mechanisms.
 */
link.updateLinks = function () {
    var data = link.updateData();
    link.removeElements(data.exit());
    link.addNewElements(data.enter());
    link.updateElements();
};

/**
 * Update the links element with data coming from dataModel.links.
 */
link.updateData = function () {
    return graph$1.svg.select("#" + link.gID).selectAll(".ppt-glink").data(dataModel.links, function (d) {
        return d.id;
    });
};

/**
 * Clean links elements removed from the list.
 */
link.removeElements = function (exitingData) {
    exitingData.remove();
};

/**
 * Create new elements.
 */
link.addNewElements = function (enteringData) {

    var newLinkElements = enteringData.append("g")
        .attr("class", "ppt-glink")
        .on("click", link.clickLink)
        .on("mouseover", link.mouseOverLink)
        .on("mouseout", link.mouseOutLink);

    newLinkElements.append("path")
        .attr("class", "ppt-link");

    newLinkElements.append("text")
        .attr("text-anchor", "middle")
        .attr("dy", link.TEXT_DY)
        .append("textPath")
        .attr("class", "ppt-textPath")
        .attr("startOffset", "50%");
};

/**
 * Update all the elements (new + modified)
 */
link.updateElements = function () {
    var toUpdateElem = graph$1.svg.select("#" + link.gID).selectAll(".ppt-glink");

    toUpdateElem
        .attr("id", function (d) {
            return "ppt-glink_" + d.id;
        });

    toUpdateElem.selectAll(".ppt-link")
        .attr("id", function (d) {
            return "ppt-path_" + d.id
        })
        .attr("stroke", function (d) {
            return provider$1.link.getColor(d, "path", "stroke");
        })
        .attr("class", function (link) {
            return "ppt-link " + provider$1.link.getCSSClass(link, "path")
        });

    // Due to a bug on webkit browsers (as of 30/01/2014) textPath cannot be selected
    // To workaround this issue the selection is done with its associated css class
    toUpdateElem.selectAll("text")
        .attr("id", function (d) {
            return "ppt-text_" + d.id
        })
        .attr("class", function (link) {
            return provider$1.link.getCSSClass(link, "text")
        })
        .attr("fill", function (d) {
            return provider$1.link.getColor(d, "text", "fill");
        })
        .selectAll(".ppt-textPath")
        .attr("id", function (d) {
            return "ppt-textpath_" + d.id;
        })
        .attr("class", function (link) {
            return "ppt-textpath " + provider$1.link.getCSSClass(link, "text-path")
        })
        .attr("xlink:href", function (d) {
            return "#ppt-path_" + d.id;
        })
        .text(function (d) {
            return provider$1.link.getTextValue(d);
        });
};

/**
 * Function called when mouse is over the link.
 * This function is used to change the CSS class on hover of the link and query viewer element.
 *
 * TODO try to introduce event instead of directly access query spans here. This could be used in future extensions.
 */
link.mouseOverLink = function () {
    d3__namespace.select(this)
        .select("path")
        .attr("class", function (link) {
            return "ppt-link " + provider$1.link.getCSSClass(link, "path--hover")
        });

    d3__namespace.select(this).select("text")
        .attr("class", function (link) {
            return provider$1.link.getCSSClass(link, "text--hover")
        });

    var hoveredLink = d3__namespace.select(this).data()[0];

    if (queryviewer.isActive) {
        queryviewer.queryConstraintSpanElements.filter(function (d) {
            return d.ref === hoveredLink;
        }).classed("hover", true);
        queryviewer.querySpanElements.filter(function (d) {
            return d.ref === hoveredLink;
        }).classed("hover", true);
    }

    if (cypherviewer.isActive) {
        cypherviewer.querySpanElements.filter(function (d) {
            return d.link === hoveredLink;
        }).classed("hover", true);
    }
};

/**
 * Function called when mouse goes out of the link.
 * This function is used to reinitialize the CSS class of the link and query viewer element.
 */
link.mouseOutLink = function () {
    d3__namespace.select(this)
        .select("path")
        .attr("class", function (link) {
            return "ppt-link " + provider$1.link.getCSSClass(link, "path")
        });

    d3__namespace.select(this).select("text")
        .attr("class", function (link) {
            return provider$1.link.getCSSClass(link, "text")
        });

    var hoveredLink = d3__namespace.select(this).data()[0];

    if (queryviewer.isActive) {
        queryviewer.queryConstraintSpanElements.filter(function (d) {
            return d.ref === hoveredLink;
        }).classed("hover", false);
        queryviewer.querySpanElements.filter(function (d) {
            return d.ref === hoveredLink;
        }).classed("hover", false);
    }

    if (cypherviewer.isActive) {
        cypherviewer.querySpanElements.filter(function (d) {
            return d.link === hoveredLink;
        }).classed("hover", false);
    }
};

// Delete all related nodes from this link
link.clickLink = function () {
    var clickedLink = d3__namespace.select(this).data()[0];

    if (clickedLink.type !== link.LinkTypes.VALUE) {
        // Collapse all expanded choose nodes first to avoid having invalid displayed value node if collapsed relation contains a value.
        graph$1.node.collapseAllNode();

        var willChangeResults = graph$1.node.removeNode(clickedLink.target);

        graph$1.hasGraphChanged = true;
        result.hasChanged = willChangeResults;
        update();
    }

};

/**
 * Convert the parameter to a function returning the parameter if it is not already a function.
 *
 * @param param
 * @return {*}
 */

function toFunction(param) {
    if (typeof param === "function") {
        return param;
    } else {
        return function () {
            return param;
        };
    }
}

var CONTEXT_2D = document.createElement("canvas").getContext("2d");
var DEFAULT_CANVAS_LINE_HEIGHT = 12;

function measureTextWidth(text) {
    return CONTEXT_2D.measureText(text).width;
}

/**
 * Compute the radius of the circle wrapping all the lines.
 *
 * @param lines array of text lines
 * @return {number}
 */
function computeTextRadius(lines) {
    var textRadius = 0;

    for (var i = 0, n = lines.length; i < n; ++i) {
        var dx = lines[i].width / 2;
        var dy = (Math.abs(i - n / 2 + 0.5) + 0.5) * DEFAULT_CANVAS_LINE_HEIGHT;
        textRadius = Math.max(textRadius, Math.sqrt(dx * dx + dy * dy));
    }

    return textRadius;
}

function computeLines(words, targetWidth) {
    var line;
    var lineWidth0 = Infinity;
    var lines = [];

    for (var i = 0, n = words.length; i < n; ++i) {
        var lineText1 = (line ? line.text + " " : "") + words[i];
        var lineWidth1 = measureTextWidth(lineText1);
        if ((lineWidth0 + lineWidth1) / 2 < targetWidth) {
            line.width = lineWidth0 = lineWidth1;
            line.text = lineText1;
        } else {
            lineWidth0 = measureTextWidth(words[i]);
            line = {width: lineWidth0, text: words[i]};
            lines.push(line);
        }
    }

    return lines;
}

function computeTargetWidth(text) {
    return Math.sqrt(measureTextWidth(text.trim()) * DEFAULT_CANVAS_LINE_HEIGHT);
}

/**
 * Split text into words.
 *
 * @param text
 * @return {*|string[]}
 */
function computeWords(text) {
    var words = text.split(/\s+/g); // To hyphenate: /\s+|(?<=-)/
    if (!words[words.length - 1]) words.pop();
    if (!words[0]) words.shift();
    return words;
}

/**
 * Inspired by https://beta.observablehq.com/@mbostock/fit-text-to-circle
 * Extract words from the text and group them by lines to fit a circle.
 *
 * @param text
 * @returns {*}
 */
function extractLines(text) {
    if (text === undefined || text === null) {
        return [];
    }

    var textString = String(text);

    var words = computeWords(textString);
    var targetWidth = computeTargetWidth(textString);

    return computeLines(words, targetWidth);
}

function createTextElements(selection, getClass) {
    selection.each(function (d) {
        var text = d3__namespace.select(this)
            .selectAll(".fitted-text")
            .data([{}]);

        text.enter()
            .append("text")
            .merge(text)
            .attr("class", "fitted-text" + (getClass !== undefined ? " " + getClass(d) : ""))
            .attr("style", "text-anchor: middle; font: 10px sans-serif");
    });
}

function createSpanElements(text, getText) {
    text.each(function (fitData) {
        var lines = extractLines(getText(fitData));
        var span = d3__namespace.select(this).selectAll("tspan")
            .data(lines);

        span.exit().remove();

        span.enter()
            .append("tspan")
            .merge(span)
            .attr("x", 0)
            .attr("y", function (d, i) {
                var lineCount = lines.length;
                return (i - lineCount / 2 + 0.8) * DEFAULT_CANVAS_LINE_HEIGHT
            })
            .text(function (d) {
                return d.text
            });
    });
}

/**
 * Create the text representation of a node by slicing the text into lines to fit the node.
 *
 * @param selection
 * @param textParam
 * @param radiusParam
 * @param classParam
 */
function appendFittedText(selection, textParam, radiusParam, classParam) {
    var getRadius = toFunction(radiusParam);
    var getText = toFunction(textParam);
    var getClass = classParam ? toFunction(classParam) : classParam;

    createTextElements(selection, getClass);

    var text = selection.select(".fitted-text");

    createSpanElements(text, getText);

    text.attr("transform", function (d) {
        var lines = extractLines(getText(d));
        var textRadius = computeTextRadius(lines);

        var scale = 1;
        if (textRadius !== 0 && textRadius) {
            scale = getRadius(d) / textRadius;
        }
        return "translate(" + 0 + "," + 0 + ")" + " scale(" + scale + ")"
    });
}

var fitTextRenderer = {};

fitTextRenderer.getNodeBoundingBox = function(node) {
    return node.getBBox();
};

/**
 * Create the text representation of a node by slicing the text into lines to fit the node.
 *
 * TODO: Clean getLines return and corresponding data.
 * @param nodeSelection
 */
fitTextRenderer.render = function (nodeSelection) {

    var backgroundRectSelection = nodeSelection
        .append("rect")
        .attr("fill", function (node) {
            return provider$1.node.getColor(node, "back-text", "fill");
        })
        .attr("class", function (node) {
            return provider$1.node.getCSSClass(node, "back-text")
        });

    appendFittedText(nodeSelection,
        function (d) { return provider$1.node.getTextValue(d)},
        function (d) { return provider$1.node.getSize(d) },
        function (d) { return provider$1.node.getCSSClass(d, "text") });

    backgroundRectSelection
        .attr("x", function (d) {
            var bbox = fitTextRenderer.getNodeBoundingBox(d3__namespace.select(this.parentNode).select("text").node());
            return bbox.x - 3;
        })
        .attr("y", function (d) {
            var bbox = fitTextRenderer.getNodeBoundingBox(d3__namespace.select(this.parentNode).select("text").node());
            return bbox.y;
        })
        .attr("rx", "5")
        .attr("ry", "5")
        .attr("width", function (d) {
            var bbox = fitTextRenderer.getNodeBoundingBox(d3__namespace.select(this.parentNode).select("text").node());
            return bbox.width + 6;
        })
        .attr("height", function (d) {
            var bbox = fitTextRenderer.getNodeBoundingBox(d3__namespace.select(this.parentNode).select("text").node());
            return bbox.height;
        })
        .attr("transform", function (d) {
            return d3__namespace.select(this.parentNode).select("text").attr("transform")
        });
};

var textRenderer = {};
textRenderer.TEXT_Y = 8; // TODO move this in dedicated config

textRenderer.getNodeBoundingBox = function(node) {
    return node.getBBox();
};

/**
 * Create the text representation of a node with a SVG rect element as background.
 *
 * TODO: clean mouseover text because this renderer change parent clip-path attribute
 * @param nodeSelection
 */
textRenderer.render = function (nodeSelection) {

    var backgroundRectSelection = nodeSelection
        .append("rect")
        .attr("fill", function (node) {
            return provider$1.node.getColor(node, "back-text", "fill");
        })
        .attr("class", function (node) {
            return provider$1.node.getCSSClass(node, "back-text")
        });

    nodeSelection.append('text')
        .attr('x', 0)
        .attr('y', textRenderer.TEXT_Y)
        .attr('text-anchor', 'middle')
        .attr("class", function (node) {
            return provider$1.node.getCSSClass(node, "text")
        })
        .on("mouseover", function (d) {
            d3__namespace.select(this.parentNode).attr("clip-path", null);
        })
        .on("mouseout", function (d) {
            d3__namespace.select(this.parentNode)
                .attr("clip-path", function (node) {
                    return "url(#node-view" + node.id + ")";
                });
        })
        .text(function (d) {
            return provider$1.node.getTextValue(d);
        });

    backgroundRectSelection
        .attr("x", function (d) {
            var bbox = textRenderer.getNodeBoundingBox(d3__namespace.select(this.parentNode).select("text").node());
            return bbox.x - 3;
        })
        .attr("y", function (d) {
            var bbox = textRenderer.getNodeBoundingBox(d3__namespace.select(this.parentNode).select("text").node());
            return bbox.y;
        })
        .attr("rx", "5")
        .attr("ry", "5")
        .attr("width", function (d) {
            var bbox = textRenderer.getNodeBoundingBox(d3__namespace.select(this.parentNode).select("text").node());
            return bbox.width + 6;
        })
        .attr("height", function (d) {
            var bbox = textRenderer.getNodeBoundingBox(d3__namespace.select(this.parentNode).select("text").node());
            return bbox.height;
        });
};

var node = {};

// ID of the g element in SVG graph containing all the link elements.
node.gID = "popoto-gnodes";

// Node ellipse size used by default for text nodes.
node.DONUTS_MARGIN = 0;
node.DONUT_WIDTH = 20;

// Define the max number of character displayed in node.
node.NODE_MAX_CHARS = 11;
node.NODE_TITLE_MAX_CHARS = 100;

// Number of nodes displayed per page during value selection.
node.PAGE_SIZE = 10;

// Count box default size
node.CountBox = {x: 16, y: 33, w: 52, h: 19};

// Store choose node state to avoid multiple node expand at the same time
node.chooseWaiting = false;

node.getDonutInnerRadius = function (n) {
    return provider$1.node.getSize(n) + node.DONUTS_MARGIN;
};

node.getDonutOuterRadius = function (n) {
    return provider$1.node.getSize(n) + node.DONUTS_MARGIN + node.DONUT_WIDTH;
};

node.pie = d3__namespace.pie()
    .sort(null)
    .value(function (d) {
        return 1;
    });

/**
 * Defines the list of possible nodes.
 * ROOT: Node used as graph root. It is the target of the query. Only one node of this type should be available in graph.
 * CHOOSE: Nodes defining a generic node label. From these node is is possible to select a value or explore relations.
 * VALUE: Unique node containing a value constraint. Usually replace CHOOSE nodes once a value as been selected.
 * GROUP: Empty node used to group relations. No value can be selected but relations can be explored. These nodes doesn't have count.
 */
node.NodeTypes = Object.freeze({ROOT: 0, CHOOSE: 1, VALUE: 2, GROUP: 3});

// Used to generate unique internal labels used for example as identifier in Cypher query.
node.internalLabels = {};

/**
 * Create a normalized identifier from a node label.
 * Multiple calls with the same node label will generate different unique identifier.
 *
 * @param nodeLabel
 * @returns {string}
 */
node.generateInternalLabel = function (nodeLabel) {
    var label = nodeLabel ? nodeLabel.toLowerCase().replace(/ /g, '') : "n";

    if (label in node.internalLabels) {
        node.internalLabels[label] = node.internalLabels[label] + 1;
    } else {
        node.internalLabels[label] = 0;
        return label;
    }

    return label + node.internalLabels[label];
};

/**
 * Update Nodes SVG elements using D3.js update mechanisms.
 */
node.updateNodes = function () {
    var data = node.updateData();
    node.removeElements(data.exit());
    node.addNewElements(data.enter());
    node.updateElements();
};

/**
 * Update node data with changes done in dataModel.nodes model.
 */
node.updateData = function () {
    var data = graph$1.svg.select("#" + node.gID).selectAll(".ppt-gnode").data(dataModel.nodes, function (d) {
        return d.id;
    });

    if (graph$1.hasGraphChanged) {
        node.updateAutoLoadValues();

        if (!graph$1.DISABLE_COUNT && !graph$1.ignoreCount) {
            node.updateCount();
        }
    }
    graph$1.hasGraphChanged = false;

    return data;
};

/**
 * Update nodes and result counts by executing a query for every nodes with the new graph structure.
 */
node.updateCount = function () {
    var statements = [];

    var countedNodes = dataModel.nodes
        .filter(function (d) {
            return d.type !== node.NodeTypes.VALUE && d.type !== node.NodeTypes.GROUP && (!d.hasOwnProperty("isNegative") || !d.isNegative);
        });

    countedNodes.forEach(function (n) {
        var nodeCountQuery = query.generateNodeCountQuery(n);
        statements.push(
            {
                "statement": nodeCountQuery.statement,
                "parameters": nodeCountQuery.parameters
            }
        );
    });

    logger.info("Count nodes ==>");
    runner.run(
        {
            "statements": statements
        })
        .then(function (results) {
            logger.info("<== Count nodes");
            var data = runner.toObject(results);

            for (var i = 0; i < countedNodes.length; i++) {
                countedNodes[i].count = data[i][0].count;
            }

            // Update result count with root node new count
            if (result.resultCountListeners.length > 0) {
                result.updateResultsCount();
            }

            node.updateElements();
            graph$1.link.updateElements();
        })
        .catch(function (error) {
            logger.error(error);
            countedNodes.forEach(function (n) {
                n.count = 0;
            });
            node.updateElements();
            graph$1.link.updateElements();
        });
};

/**
 * Update values for nodes having preloadData property
 */
node.updateAutoLoadValues = function () {
    var statements = [];

    var nodesToLoadData = node.getAutoLoadValueNodes();

    for (var i = 0; i < nodesToLoadData.length; i++) {
        var nodeToQuery = nodesToLoadData[i];
        var nodeValueQuery = query.generateNodeValueQuery(nodeToQuery);
        statements.push(
            {
                "statement": nodeValueQuery.statement,
                "parameters": nodeValueQuery.parameters
            }
        );
    }

    if (statements.length > 0) {
        logger.info("AutoLoadValue ==>");
        runner.run(
            {
                "statements": statements
            })
            .then(function (results) {
                logger.info("<== AutoLoadValue");

                var data = runner.toObject(results);

                for (var i = 0; i < nodesToLoadData.length; i++) {
                    var nodeToQuery = nodesToLoadData[i];
                    var constraintAttr = provider$1.node.getConstraintAttribute(nodeToQuery.label);
                    // Here results are parsed and values already selected are filtered out
                    nodeToQuery.data = data[i].filter(function (dataToFilter) {
                        var keepData = true;
                        if (nodeToQuery.hasOwnProperty("value") && nodeToQuery.value.length > 0) {
                            nodeToQuery.value.forEach(function (value) {
                                if (value.attributes[constraintAttr] === dataToFilter[constraintAttr]) {
                                    keepData = false;
                                }
                            });
                        }
                        return keepData;
                    });

                    nodeToQuery.page = 1;
                }

                graph$1.notifyListeners(graph$1.Events.GRAPH_NODE_DATA_LOADED, [nodesToLoadData]);
            })
            .catch(function (error) {
                logger.error(error);
            });
    }
};

/**
 * Remove old elements.
 * Should be called after updateData.
 */
node.removeElements = function (exitingData) {
    // Nodes without parent are simply removed.
    exitingData.filter(function (d) {
        return !d.parent;
    }).remove();

    // Nodes with a parent are removed with an animation (nodes are collapsed to their parents before being removed)
    exitingData.filter(function (d) {
        return d.parent;
    }).transition().duration(300).attr("transform", function (d) {
        return "translate(" + d.parent.x + "," + d.parent.y + ")";
    }).remove();
};

/**
 * Add all new elements.
 * Only the skeleton of new nodes are added custom data will be added during the element update phase.
 * Should be called after updateData and before updateElements.
 */
node.addNewElements = function (enteringData) {

    var gNewNodeElements = enteringData
        .append("g")
        .attr("class", "ppt-gnode");

    gNewNodeElements.on("click", node.nodeClick)
        .on("mouseover", node.mouseOverNode)
        // .on("mousemove", nUdeXXX.mouseMoveNode)
        .on("mouseout", node.mouseOutNode);

    // Add right click on all nodes except value
    gNewNodeElements.filter(function (d) {
        return d.type !== node.NodeTypes.VALUE;
    }).on("contextmenu", node.clearSelection);

    // Disable right click context menu on value nodes
    gNewNodeElements.filter(function (d) {
        return d.type === node.NodeTypes.VALUE;
    }).on("contextmenu", function () {
        // Disable context menu on
        d3__namespace.event.preventDefault();
    });

    var nodeDefs = gNewNodeElements.append("defs");

    // Circle clipPath using node radius size
    nodeDefs.append("clipPath")
        .attr("id", function (n) {
            return "node-view" + n.id;
        })
        .append("circle")
        .attr("cx", 0)
        .attr("cy", 0);

    // Nodes are composed of 3 layouts and skeleton are created here.
    node.addBackgroundElements(gNewNodeElements);
    node.addMiddlegroundElements(gNewNodeElements);
    node.addForegroundElements(gNewNodeElements);
};

/**
 * Create the background for a new node element.
 * The background of a node is defined by a circle not visible by default (fill-opacity set to 0) but can be used to highlight a node with animation on this attribute.
 * This circle also define the node zone that can receive events like mouse clicks.
 *
 * @param gNewNodeElements
 */
node.addBackgroundElements = function (gNewNodeElements) {
    var background = gNewNodeElements
        .append("g")
        .attr("class", "ppt-g-node-background")
        .classed("hide", graph$1.DISABLE_RELATION);

    background.append("g")
        .attr("class", "ppt-donut-labels");
    background.append("g")
        .attr("class", "ppt-donut-segments");
};

/**
 * Create the node main elements.
 *
 * @param gNewNodeElements
 */
node.addMiddlegroundElements = function (gNewNodeElements) {
    gNewNodeElements
        .append("g")
        .attr("class", "ppt-g-node-middleground");
};

/**
 * Create the node foreground elements.
 * It contains node additional elements, count or tools like navigation arrows.
 *
 * @param gNewNodeElements
 */
node.addForegroundElements = function (gNewNodeElements) {
    var foreground = gNewNodeElements
        .append("g")
        .attr("class", "ppt-g-node-foreground");

    // Arrows icons added only for root and choose nodes
    var gArrow = foreground.filter(function (d) {
        return d.type === node.NodeTypes.ROOT || d.type === node.NodeTypes.CHOOSE;
    })
        .append("g")
        .attr("class", "ppt-node-foreground-g-arrows");

    var glArrow = gArrow.append("g");
    //glArrow.append("polygon")
    //.attr("points", "-53,-23 -33,-33 -33,-13");
    glArrow.append("circle")
        .attr("class", "ppt-larrow")
        .attr("cx", "-43")
        .attr("cy", "-23")
        .attr("r", "17");

    glArrow.append("path")
        .attr("class", "ppt-arrow")
        .attr("d", "m -44.905361,-23 6.742,-6.742 c 0.81,-0.809 0.81,-2.135 0,-2.944 l -0.737,-0.737 c -0.81,-0.811 -2.135,-0.811 -2.945,0 l -8.835,8.835 c -0.435,0.434 -0.628,1.017 -0.597,1.589 -0.031,0.571 0.162,1.154 0.597,1.588 l 8.835,8.834 c 0.81,0.811 2.135,0.811 2.945,0 l 0.737,-0.737 c 0.81,-0.808 0.81,-2.134 0,-2.943 l -6.742,-6.743 z");

    glArrow.on("click", function (clickedNode) {
        d3__namespace.event.stopPropagation(); // To avoid click event on svg element in background

        // On left arrow click page number is decreased and node expanded to display the new page
        if (clickedNode.page > 1) {
            clickedNode.page--;
            node.collapseNode(clickedNode);
            node.expandNode(clickedNode);
        }
    });

    var grArrow = gArrow.append("g");
    //grArrow.append("polygon")
    //.attr("points", "53,-23 33,-33 33,-13");

    grArrow.append("circle")
        .attr("class", "ppt-rarrow")
        .attr("cx", "43")
        .attr("cy", "-23")
        .attr("r", "17");

    grArrow.append("path")
        .attr("class", "ppt-arrow")
        .attr("d", "m 51.027875,-24.5875 -8.835,-8.835 c -0.811,-0.811 -2.137,-0.811 -2.945,0 l -0.738,0.737 c -0.81,0.81 -0.81,2.136 0,2.944 l 6.742,6.742 -6.742,6.742 c -0.81,0.81 -0.81,2.136 0,2.943 l 0.737,0.737 c 0.81,0.811 2.136,0.811 2.945,0 l 8.835,-8.836 c 0.435,-0.434 0.628,-1.017 0.597,-1.588 0.032,-0.569 -0.161,-1.152 -0.596,-1.586 z");

    grArrow.on("click", function (clickedNode) {
        d3__namespace.event.stopPropagation(); // To avoid click event on svg element in background

        if (clickedNode.page * node.PAGE_SIZE < clickedNode.count) {
            clickedNode.page++;
            node.collapseNode(clickedNode);
            node.expandNode(clickedNode);
        }
    });

    // Count box
    if (!graph$1.DISABLE_COUNT) {
        var countForeground = foreground.filter(function (d) {
            return d.type !== node.NodeTypes.GROUP;
        });

        countForeground
            .append("rect")
            .attr("x", node.CountBox.x)
            .attr("y", node.CountBox.y)
            .attr("width", node.CountBox.w)
            .attr("height", node.CountBox.h)
            .attr("class", "ppt-count-box");

        countForeground
            .append("text")
            .attr("x", 42)
            .attr("y", 48)
            .attr("text-anchor", "middle")
            .attr("class", "ppt-count-text");
    }

    foreground.filter(function (d) {
        return d.type === node.NodeTypes.CHOOSE;
    }).append("g")
        .attr("class", "ppt-g-node-ban")
        .append("path")
        .attr("d", "M89.1 19.2C88 17.7 86.6 16.2 85.2 14.8 83.8 13.4 82.3 12 80.8 10.9 72 3.9 61.3 0 50 0 36.7 0 24.2 5.4 14.8 14.8 5.4 24.2 0 36.7 0 50c0 11.4 3.9 22.1 10.9 30.8 1.2 1.5 2.5 3 3.9 4.4 1.4 1.4 2.9 2.7 4.4 3.9C27.9 96.1 38.6 100 50 100 63.3 100 75.8 94.6 85.2 85.2 94.6 75.8 100 63.3 100 50 100 38.7 96.1 28 89.1 19.2ZM11.9 50c0-10.2 4-19.7 11.1-27C30.3 15.9 39.8 11.9 50 11.9c8.2 0 16 2.6 22.4 7.3L19.3 72.4C14.5 66 11.9 58.2 11.9 50Zm65 27c-7.2 7.1-16.8 11.1-27 11.1-8.2 0-16-2.6-22.4-7.4L80.8 27.6C85.5 34 88.1 41.8 88.1 50c0 10.2-4 19.7-11.1 27z");
};

/**
 * Updates all elements.
 */
node.updateElements = function () {
    var toUpdateElem = graph$1.svg.select("#" + node.gID).selectAll(".ppt-gnode");

    toUpdateElem.attr("id", function (d) {
        return "popoto-gnode_" + d.id;
    });

    if (graph$1.USE_VORONOI_LAYOUT) {
        toUpdateElem.attr("clip-path", function (d) {
            return "url(#voroclip-" + d.id + ")";
        });
    }

    toUpdateElem.select("defs")
        .select("clipPath")
        .attr("id", function (n) {
            return "node-view" + n.id;
        }).selectAll("circle")
        .attr("r", function (n) {
            return provider$1.node.getSize(n);
        });

    // Display voronoi paths
    // TODO ZZZ re|move
    // nUdeXXX.selectAllData.selectAll(".gra").data(["unique"]).enter().append("g").attr("class", "gra").append("use");
    // nUdeXXX.selectAllData.selectAll("use").attr("xlink:href",function(d){
    //     console.log("#pvoroclip-"+d3.select(this.parentNode.parentNode).datum().id);
    //     return "#pvoroclip-"+d3.select(this.parentNode.parentNode).datum().id;
    // }).attr("fill","none").attr("stroke","red").attr("stroke-width","1px");

    // TODO ZZZ move functions?
    toUpdateElem.filter(function (n) {
        return n.type !== node.NodeTypes.ROOT
    }).call(d3__namespace.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

    function dragstarted(d) {
        if (!d3__namespace.event.active) graph$1.force.alphaTarget(0.3).restart();
        d.fx = d.x;
        d.fy = d.y;
    }

    function dragged(d) {
        d.fx = d3__namespace.event.x;
        d.fy = d3__namespace.event.y;
    }

    function dragended(d) {
        if (!d3__namespace.event.active) graph$1.force.alphaTarget(0);
        if (d.fixed === false) {
            d.fx = null;
            d.fy = null;
        }

    }

    node.updateBackgroundElements();
    node.updateMiddlegroundElements();
    node.updateForegroundElements();
};

node.updateBackgroundElements = function () {
    var nodeBackgroundElements = graph$1.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-background");

    nodeBackgroundElements.select(".ppt-donut-labels").selectAll("*").remove();
    nodeBackgroundElements.select(".ppt-donut-segments").selectAll("*").remove();

    var gSegment = nodeBackgroundElements.select(".ppt-donut-segments").selectAll(".ppt-segment-container")
        .data(function (d) {
            var relationships = [];
            if (d.hasOwnProperty("relationships")) {
                relationships = d.relationships;
            }
            return relationships;
        }, function (d) {
            return d.id;
        })
        .enter()
        .append("g")
        .attr("class", ".ppt-segment-container")
        .on("click", node.segmentClick)
        .on("mouseover", function (d) {
            d3__namespace.select(this).select(".ppt-text-arc").classed("hover", true);
        })
        .on("mouseout", function (d) {
            d3__namespace.select(this).select(".ppt-text-arc").classed("hover", false);
        });

    gSegment.append("title").attr("class", "ppt-svg-title")
        .text(function (d) {
            return d.label + " " + d.target;
        });

    var gLabel = nodeBackgroundElements.select(".ppt-donut-labels").selectAll(".ppt-segment-container")
        .data(function (n) {
            var relationships = [];
            if (n.hasOwnProperty("relationships")) {
                relationships = n.relationships;
            }
            return relationships;
        }, function (relationship) {
            return relationship.id;
        })
        .enter()
        .append("g")
        .attr("class", ".ppt-segment-container")
        .on("click", node.segmentClick)
        .on("mouseover", function (d) {
            d3__namespace.select(this).select(".ppt-text-arc").classed("hover", true);
        })
        .on("mouseout", function (d) {
            d3__namespace.select(this).select(".ppt-text-arc").classed("hover", false);
        });

    gLabel.append("path")
        .attr("class", "ppt-hidden-arc")
        .attr("id", function (d, i) {
            var n = d3__namespace.select(this.parentNode.parentNode).datum();
            return "arc_" + n.id + "_" + i;
        })
        .attr("d", function (relationship) {
            var n = d3__namespace.select(this.parentNode.parentNode).datum();

            //A regular expression that captures all in between the start of a string (denoted by ^)
            //and the first capital letter L
            var firstArcSection = /(^.+?)L/;
            var singleArcSection = /(^.+?)M/;

            var intermediateArc = {
                startAngle: relationship.directionAngle - (Math.PI - 0.1),
                endAngle: relationship.directionAngle + (Math.PI - 0.1)
            };

            var arcPath = d3__namespace.arc()
                .innerRadius(node.getDonutInnerRadius(n))
                .outerRadius(node.getDonutOuterRadius(n))(intermediateArc);

            //The [1] gives back the expression between the () (thus not the L as well)
            //which is exactly the arc statement
            var res = firstArcSection.exec(arcPath);
            var newArc = "";
            if (res && res.length > 1) {
                newArc = res[1];
            } else {
                newArc = singleArcSection.exec(arcPath)[1];
            }

            //Replace all the comma's so that IE can handle it -_-
            //The g after the / is a modifier that "find all matches rather than stopping after the first match"
            newArc = newArc.replace(/,/g, " ");

            return newArc;
        })
        .style("fill", "none")
        .style("stroke", "none");

    gSegment.append("text")
        .attr("text-anchor", "middle")
        .attr("class", function (d) {
            var n = d3__namespace.select(this.parentNode.parentNode).datum();
            if (n.hasOwnProperty("count") && n.count === 0) {
                return "ppt-text-arc disabled";
            } else {
                return "ppt-text-arc";
            }
        })
        .attr("fill", function (d) {
            var n = d3__namespace.select(this.parentNode.parentNode).datum();

            return provider$1.link.getColor({
                label: d.label,
                type: graph$1.link.LinkTypes.SEGMENT,
                source: n,
                target: {label: d.target}
            }, "segment", "fill");
        })
        .attr("dy", graph$1.link.TEXT_DY)
        .append("textPath")
        .attr("startOffset", "50%")
        .attr("xlink:href", function (d, i) {
            var n = d3__namespace.select(this.parentNode.parentNode.parentNode).datum();
            return "#arc_" + n.id + "_" + i;
        })
        .text(function (d) {
            var n = d3__namespace.select(this.parentNode.parentNode.parentNode).datum();

            return provider$1.link.getTextValue({
                source: n,
                target: {label: d.target},
                label: d.label,
                type: graph$1.link.LinkTypes.SEGMENT
            });
        });

    gSegment.append("path")
        .attr("class", function (d) {
            var n = d3__namespace.select(this.parentNode.parentNode).datum();
            if (n.hasOwnProperty("count") && n.count === 0) {
                return "ppt-segment disabled";
            } else {
                return "ppt-segment";
            }
        })
        .attr("d", function (d) {
            var n = d3__namespace.select(this.parentNode.parentNode).datum();
            return d3__namespace.arc()
                .innerRadius(node.getDonutInnerRadius(n))
                .outerRadius(node.getDonutOuterRadius(n))(d)
        })
        .attr("fill", function (d) {
            var n = d3__namespace.select(this.parentNode.parentNode).datum();
            return provider$1.link.getColor({
                label: d.label,
                type: graph$1.link.LinkTypes.RELATION,
                source: n,
                target: {label: d.target}
            }, "path", "fill");
        })
        .attr("stroke", function (d) {
            var n = d3__namespace.select(this.parentNode.parentNode).datum();

            return provider$1.link.getColor({
                label: d.label,
                type: graph$1.link.LinkTypes.RELATION,
                source: n,
                target: {label: d.target}
            }, "path", "stroke");
        })
    ;

};

/**
 * Update the middle layer of nodes.
 * TODO refactor node generation to allow future extensions (for example add plugin with new node types...)
 */
node.updateMiddlegroundElements = function () {
    var middleG = graph$1.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-middleground");

    middleG.attr("clip-path", function (n) {
        return "url(#node-view" + n.id + ")";
    });

    // Clear all content in case node type has changed
    middleG.selectAll("*").remove();


    node.updateMiddlegroundElementsTooltip(middleG);

    node.updateMiddlegroundElementsText(middleG.filter(function (d) {
        return provider$1.node.getNodeDisplayType(d) === provider$1.node.DisplayTypes.TEXT;
    }));

    node.updateMiddlegroundElementsImage(middleG.filter(function (d) {
        return provider$1.node.getNodeDisplayType(d) === provider$1.node.DisplayTypes.IMAGE;
    }));

    node.updateMiddlegroundElementsSymbol(middleG.filter(function (d) {
        return provider$1.node.getNodeDisplayType(d) === provider$1.node.DisplayTypes.SYMBOL;
    }));

    node.updateMiddlegroundElementsSVG(middleG.filter(function (d) {
        return provider$1.node.getNodeDisplayType(d) === provider$1.node.DisplayTypes.SVG;
    }));

    node.updateMiddlegroundElementsDisplayedText(middleG.filter(function (d) {
        return provider$1.node.isTextDisplayed(d);
    }));

};

node.updateMiddlegroundElementsTooltip = function (middleG) {
    // Most browser will generate a tooltip if a title is specified for the SVG element
    // TODO Introduce an SVG tooltip instead?
    middleG.append("title")
        .attr("class", function (n) {
            return provider$1.node.getCSSClass(n, "title")
        })
        .text(function (d) {
            return provider$1.node.getTextValue(d, node.NODE_TITLE_MAX_CHARS);
        });

};

node.updateMiddlegroundElementsText = function (gMiddlegroundTextNodes) {
    var circle = gMiddlegroundTextNodes.append("circle").attr("r", function (n) {
        return provider$1.node.getSize(n);
    });

    // Set class according to node type
    circle
        .attr("class", function (n) {
            return provider$1.node.getCSSClass(n, "circle")
        })
        .attr("fill", function (n) {
            return provider$1.node.getColor(n, "circle", "fill");
        })
        .attr("stroke", function (n) {
            return provider$1.node.getColor(n, "circle", "stroke");
        });
};

node.updateMiddlegroundElementsImage = function (gMiddlegroundImageNodes) {
    gMiddlegroundImageNodes.append("circle").attr("r", function (n) {
        return provider$1.node.getSize(n);
    })
        .attr("class", function (n) {
            return provider$1.node.getCSSClass(n, "image-background-circle")
        });

    gMiddlegroundImageNodes.append("image")
        .attr("class", function (n) {
            return provider$1.node.getCSSClass(n, "image")
        })
        .attr("width", function (d) {
            return provider$1.node.getImageWidth(d);
        })
        .attr("height", function (d) {
            return provider$1.node.getImageHeight(d);
        })
        // Center the image on node
        .attr("transform", function (d) {
            return "translate(" + (-provider$1.node.getImageWidth(d) / 2) + "," + (-provider$1.node.getImageHeight(d) / 2) + ")";
        })
        .attr("xlink:href", function (d) {
            return provider$1.node.getImagePath(d);
        });
};

node.updateMiddlegroundElementsSymbol = function (gMiddlegroundSymbolNodes) {
    gMiddlegroundSymbolNodes.append("circle").attr("r", function (n) {
        return provider$1.node.getSize(n);
    })
        .attr("class", function (n) {
            return provider$1.node.getCSSClass(n, "symbol-background-circle")
        })
        .attr("fill", function (n) {
            return provider$1.node.getColor(n, "circle", "fill");
        })
        .attr("stroke", function (n) {
            return provider$1.node.getColor(n, "circle", "stroke");
        });

    gMiddlegroundSymbolNodes.append("use")
        .attr("class", function (n) {
            return provider$1.node.getCSSClass(n, "symbol")
        })
        .attr("width", function (d) {
            return provider$1.node.getImageWidth(d);
        })
        .attr("height", function (d) {
            return provider$1.node.getImageHeight(d);
        })
        // Center the image on node
        .attr("transform", function (d) {
            return "translate(" + (-provider$1.node.getImageWidth(d) / 2) + "," + (-provider$1.node.getImageHeight(d) / 2) + ")";
        })
        .attr("xlink:href", function (d) {
            return provider$1.node.getImagePath(d);
        })
        .attr("fill", function (n) {
            return provider$1.node.getColor(n, "circle", "fill");
        })
        .attr("stroke", function (n) {
            return provider$1.node.getColor(n, "circle", "stroke");
        });
};

node.updateMiddlegroundElementsSVG = function (gMiddlegroundSVGNodes) {
    var SVGmiddleG = gMiddlegroundSVGNodes.append("g");

    SVGmiddleG.append("circle").attr("r", function (n) {
        return provider$1.node.getSize(n);
    }).attr("class", "ppt-svg-node-background");

    var svgMiddlePaths = SVGmiddleG.selectAll("path").data(function (d) {
        return provider$1.node.getSVGPaths(d);
    });

    // Update nested data elements
    svgMiddlePaths.exit().remove();
    svgMiddlePaths.enter().append("path");

    SVGmiddleG
        .selectAll("path")
        .attr("class", function (d) {
            var n = d3__namespace.select(this.parentNode).datum();
            return provider$1.node.getCSSClass(n, "path")
        })
        .each(function (d, i) {
            for (var prop in d) {
                if (d.hasOwnProperty(prop)) {
                    d3__namespace.select(this).attr(prop, d[prop]);
                }
            }
        });
};

node.updateMiddlegroundElementsDisplayedText = function (middleG) {
    var textDisplayed = middleG.filter(function (d) {
        return provider$1.node.isTextDisplayed(d);
    });

    if (graph$1.USE_FIT_TEXT) {
        fitTextRenderer.render(textDisplayed);
    } else {
        textRenderer.render(textDisplayed);
    }
};

/**
 * Updates the foreground elements
 */
node.updateForegroundElements = function () {

    // Updates browse arrows status
    // TODO ZZZ extract variable?
    var gArrows = graph$1.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-foreground")
        .selectAll(".ppt-node-foreground-g-arrows");
    gArrows.classed("active", function (d) {
        return d.valueExpanded && d.data && d.data.length > node.PAGE_SIZE;
    });

    gArrows.selectAll(".ppt-larrow").classed("enabled", function (d) {
        return d.page > 1;
    });

    gArrows.selectAll(".ppt-rarrow").classed("enabled", function (d) {
        if (d.data) {
            var count = d.data.length;
            return d.page * node.PAGE_SIZE < count;
        } else {
            return false;
        }
    });

    // Update count box class depending on node type
    var gForegrounds = graph$1.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-foreground");

    gForegrounds.selectAll(".ppt-count-box").filter(function (d) {
        return d.type !== node.NodeTypes.CHOOSE;
    }).classed("root", true);

    gForegrounds.selectAll(".ppt-count-box").filter(function (d) {
        return d.type === node.NodeTypes.CHOOSE;
    }).classed("value", true);

    gForegrounds.selectAll(".ppt-count-box").classed("disabled", function (d) {
        return d.count === 0;
    });

    if (!graph$1.DISABLE_COUNT) {
        gForegrounds.selectAll(".ppt-count-text")
            .text(function (d) {
                if (d.count !== null) {
                    return d.count;
                } else {
                    return "...";
                }
            })
            .classed("disabled", function (d) {
                return d.count === 0;
            });
    }

    graph$1.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-foreground").filter(function (n) {
        return n.isNegative === true;
    }).selectAll(".ppt-g-node-ban")
        .attr("transform", function (d) {
            return "translate(" + (-provider$1.node.getSize(d)) + "," + (-provider$1.node.getSize(d)) + ") " +
                "scale(" + ((provider$1.node.getSize(d) * 2) / 100) + ")"; // 100 is the size of the image drawn with the path
        })
        .attr("stroke-width", function (d) {
            return (2 / ((provider$1.node.getSize(d) * 2) / 100)) + "px";
        });


    graph$1.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-foreground").selectAll(".ppt-g-node-ban")
        .classed("active", function (n) {
            return n.isNegative === true;
        });
};

node.segmentClick = function (d) {
    d3__namespace.event.preventDefault();

    var n = d3__namespace.select(this.parentNode.parentNode).datum();

    graph$1.ignoreCount = true;

    graph$1.addRelationshipData(n, d, function (targetNode) {
        graph$1.notifyListeners(graph$1.Events.GRAPH_NODE_RELATION_ADD, [
            dataModel.links.filter(function (l) {
                return l.target === targetNode;
            })
        ]);
        graph$1.ignoreCount = false;
        graph$1.hasGraphChanged = true;
        update();
    });
};

/**
 * Handle the mouse over event on nodes.
 */
node.mouseOverNode = function () {
    d3__namespace.event.preventDefault();

    // TODO don't work on IE (nodes unstable) find another way to move node in foreground on mouse over?
    // d3.select(this).moveToFront();

    // tootip.div.style("display", "inline");

    var hoveredNode = d3__namespace.select(this).data()[0];

    if (queryviewer.isActive) {
        // Hover the node in query
        queryviewer.queryConstraintSpanElements.filter(function (d) {
            return d.ref === hoveredNode;
        }).classed("hover", true);
        queryviewer.querySpanElements.filter(function (d) {
            return d.ref === hoveredNode;
        }).classed("hover", true);
    }

    if (cypherviewer.isActive) {
        cypherviewer.querySpanElements.filter(function (d) {
            return d.node === hoveredNode;
        }).classed("hover", true);
    }
};

// nUdeXXX.mouseMoveNode = function () {
//     d3.event.preventDefault();
//
//     var hoveredNode = d3.select(this).data()[0];
//
//     tootip.div
//         .text(provider.node.getTextValue(hoveredNode, nUdeXXX.NODE_TITLE_MAX_CHARS))
//         .style("left", (d3.event.pageX - 34) + "px")
//         .style("top", (d3.event.pageY - 12) + "px");
// };

/**
 * Handle mouse out event on nodes.
 */
node.mouseOutNode = function () {
    d3__namespace.event.preventDefault();

    // tootip.div.style("display", "none");

    var hoveredNode = d3__namespace.select(this).data()[0];

    if (queryviewer.isActive) {
        // Remove hover class on node.
        queryviewer.queryConstraintSpanElements.filter(function (d) {
            return d.ref === hoveredNode;
        }).classed("hover", false);
        queryviewer.querySpanElements.filter(function (d) {
            return d.ref === hoveredNode;
        }).classed("hover", false);
    }

    if (cypherviewer.isActive) {
        cypherviewer.querySpanElements.filter(function (d) {
            return d.node === hoveredNode;
        }).classed("hover", false);
    }
};

/**
 * Handle the click event on nodes.
 */
node.nodeClick = function () {
    if (!d3__namespace.event.defaultPrevented) { // To avoid click on drag end
        var clickedNode = d3__namespace.select(this).data()[0]; // Clicked node data
        logger.debug("nodeClick (" + clickedNode.label + ")");

        if (clickedNode.type === node.NodeTypes.VALUE) {
            node.valueNodeClick(clickedNode);
        } else if (clickedNode.type === node.NodeTypes.CHOOSE || clickedNode.type === node.NodeTypes.ROOT) {
            if (d3__namespace.event.ctrlKey) {
                if (clickedNode.type === node.NodeTypes.CHOOSE) {
                    clickedNode.isNegative = !clickedNode.hasOwnProperty("isNegative") || !clickedNode.isNegative;

                    node.collapseAllNode();

                    if (clickedNode.hasOwnProperty("value") && clickedNode.value.length > 0) ; else {

                        if (clickedNode.isNegative) {
                            // Remove all related nodes
                            for (var i = dataModel.links.length - 1; i >= 0; i--) {
                                if (dataModel.links[i].source === clickedNode) {
                                    node.removeNode(dataModel.links[i].target);
                                }
                            }

                            clickedNode.count = 0;
                        }
                    }

                    result.hasChanged = true;
                    graph$1.hasGraphChanged = true;
                    update();
                } // negation not supported on root node
            } else {
                if (clickedNode.valueExpanded) {
                    node.collapseNode(clickedNode);
                } else {
                    node.chooseNodeClick(clickedNode);
                }
            }
        }
    }
};

/**
 * Remove all the value node directly linked to clicked node.
 *
 * @param clickedNode
 */
node.collapseNode = function (clickedNode) {
    if (clickedNode.valueExpanded) { // node is collapsed only if it has been expanded first
        logger.debug("collapseNode (" + clickedNode.label + ")");

        graph$1.notifyListeners(graph$1.Events.GRAPH_NODE_VALUE_COLLAPSE, [clickedNode]);

        var linksToRemove = dataModel.links.filter(function (l) {
            return l.source === clickedNode && l.type === graph$1.link.LinkTypes.VALUE;
        });

        // Remove children nodes from model
        linksToRemove.forEach(function (l) {
            dataModel.nodes.splice(dataModel.nodes.indexOf(l.target), 1);
        });

        // Remove links from model
        for (var i = dataModel.links.length - 1; i >= 0; i--) {
            if (linksToRemove.indexOf(dataModel.links[i]) >= 0) {
                dataModel.links.splice(i, 1);
            }
        }

        // Node has been fixed when expanded so we unfix it back here.
        if (clickedNode.type !== node.NodeTypes.ROOT) {
            clickedNode.fixed = false;
            clickedNode.fx = null;
            clickedNode.fy = null;
        }

        // Parent node too if not root
        if (clickedNode.parent && clickedNode.parent.type !== node.NodeTypes.ROOT) {
            clickedNode.parent.fixed = false;
            clickedNode.parent.fx = null;
            clickedNode.parent.fy = null;
        }

        clickedNode.valueExpanded = false;
        update();

    } else {
        logger.debug("collapseNode called on an unexpanded node");
    }
};

/**
 * Collapse all nodes with value expanded.
 *
 */
node.collapseAllNode = function () {
    dataModel.nodes.forEach(function (n) {
        if ((n.type === node.NodeTypes.CHOOSE || n.type === node.NodeTypes.ROOT) && n.valueExpanded) {
            node.collapseNode(n);
        }
    });
};

/**
 * Function called on a value node click.
 * In this case the value is added in the parent node and all the value nodes are collapsed.
 *
 * @param clickedNode
 */
node.valueNodeClick = function (clickedNode) {
    logger.debug("valueNodeClick (" + clickedNode.label + ")");

    graph$1.notifyListeners(graph$1.Events.GRAPH_NODE_ADD_VALUE, [clickedNode]);

    if (clickedNode.parent.value === undefined) {
        clickedNode.parent.value = [];
    }
    clickedNode.parent.value.push(clickedNode);
    result.hasChanged = true;
    graph$1.hasGraphChanged = true;

    node.collapseNode(clickedNode.parent);
};

/**
 * Function called on choose node click.
 * In this case a query is executed to get all the possible value
 * @param clickedNode
 * TODO optimize with cached data?
 */
node.chooseNodeClick = function (clickedNode) {
    logger.debug("chooseNodeClick (" + clickedNode.label + ") with waiting state set to " + node.chooseWaiting);
    if (!node.chooseWaiting && !clickedNode.immutable && !(clickedNode.count === 0)) {

        // Collapse all expanded nodes first
        node.collapseAllNode();

        // Set waiting state to true to avoid multiple call on slow query execution
        node.chooseWaiting = true;

        // Don't run query to get value if node isAutoLoadValue is set to true
        if (clickedNode.data !== undefined && clickedNode.isAutoLoadValue) {
            clickedNode.page = 1;
            node.expandNode(clickedNode);
            node.chooseWaiting = false;
        } else {
            logger.info("Values (" + clickedNode.label + ") ==>");
            var nodeValueQuery = query.generateNodeValueQuery(clickedNode);
            runner.run(
                {
                    "statements": [
                        {
                            "statement": nodeValueQuery.statement,
                            "parameters": nodeValueQuery.parameters
                        }]
                })
                .then(function (results) {
                    logger.info("<== Values (" + clickedNode.label + ")");
                    var parsedData = runner.toObject(results);
                    var constraintAttr = provider$1.node.getConstraintAttribute(clickedNode.label);

                    clickedNode.data = parsedData[0].filter(function (dataToFilter) {
                        var keepData = true;
                        if (clickedNode.hasOwnProperty("value") && clickedNode.value.length > 0) {
                            clickedNode.value.forEach(function (value) {
                                if (value.attributes[constraintAttr] === dataToFilter[constraintAttr]) {
                                    keepData = false;
                                }
                            });
                        }
                        return keepData;
                    });

                    clickedNode.page = 1;
                    node.expandNode(clickedNode);
                    node.chooseWaiting = false;
                })
                .catch(function (error) {
                    node.chooseWaiting = false;
                    logger.error(error);
                });
        }
    }
};

/**
 * Add in all expanded choose nodes the value containing the specified value for the given attribute.
 * And remove it from the nodes data.
 *
 * @param attribute
 * @param value
 */
node.addExpandedValue = function (attribute, value) {

    var isAnyChangeDone = false;

    // For each expanded nodes
    for (var i = dataModel.nodes.length - 1; i >= 0; i--) {
        if (dataModel.nodes[i].valueExpanded) {

            // Look in node data if value can be found in reverse order to be able to remove value without effect on iteration index
            for (var j = dataModel.nodes[i].data.length - 1; j >= 0; j--) {
                if (dataModel.nodes[i].data[j][attribute] === value) {
                    isAnyChangeDone = true;

                    // Create field value if needed
                    if (!dataModel.nodes[i].hasOwnProperty("value")) {
                        dataModel.nodes[i].value = [];
                    }

                    // Add value
                    dataModel.nodes[i].value.push({
                        attributes: dataModel.nodes[i].data[j]
                    });

                    // Remove data added in value
                    dataModel.nodes[i].data.splice(j, 1);
                }
            }

            // Refresh node
            node.collapseNode(dataModel.nodes[i]);
            node.expandNode(dataModel.nodes[i]);
        }
    }

    if (isAnyChangeDone) {
        result.hasChanged = true;
        graph$1.hasGraphChanged = true;
        update();
    }
};

/**
 * Get all nodes that contains a value.
 *
 * @param label If set return only node of this label.
 * @return {Array} Array of nodes containing at least one value.
 */
node.getContainingValue = function (label) {
    var nodesWithValue = [];
    var links = dataModel.links, nodes = dataModel.nodes;

    if (nodes.length > 0) {

        var rootNode = nodes[0];

        // Add root value
        if (rootNode.value !== undefined && rootNode.value.length > 0) {
            if (label === undefined || label === rootNode.label) {
                nodesWithValue.push(rootNode);
            }
        }

        links.forEach(function (l) {
            var targetNode = l.target;
            if (l.type === graph$1.link.LinkTypes.RELATION && targetNode.value !== undefined && targetNode.value.length > 0) {
                if (label === undefined || label === targetNode.label) {
                    nodesWithValue.push(targetNode);
                }
            }
        });
    }

    return nodesWithValue;
};


/**
 * Add value in all CHOOSE nodes with specified label.
 *
 * @param label nodes where to insert
 * @param value
 */
node.addValueForLabel = function (label, value) {
    var isAnyChangeDone = false;

    // Find choose node with label
    for (var i = dataModel.nodes.length - 1; i >= 0; i--) {
        if (dataModel.nodes[i].type === node.NodeTypes.CHOOSE && dataModel.nodes[i].label === label) {

            // Create field value if needed
            if (!dataModel.nodes[i].hasOwnProperty("value")) {
                dataModel.nodes[i].value = [];
            }

            // check if value already exists
            var isValueFound = false;
            var constraintAttr = provider$1.node.getConstraintAttribute(label);
            dataModel.nodes[i].value.forEach(function (val) {
                if (val.attributes.hasOwnProperty(constraintAttr) && val.attributes[constraintAttr] === value.attributes[constraintAttr]) {
                    isValueFound = true;
                }
            });

            if (!isValueFound) {
                // Add value
                dataModel.nodes[i].value.push(value);
                isAnyChangeDone = true;
            }
        }
    }

    return isAnyChangeDone;
};

/**
 * Add a value in a node with the given id and the value of the first attribute if found in its data.
 *
 * @param nodeIds a list of node ids where to add the value.
 * @param displayAttributeValue the value to find in data and to add if found
 */
node.addValue = function (nodeIds, displayAttributeValue) {
    var isAnyChangeDone = false;

    // Find choose node with label
    for (var i = 0; i < dataModel.nodes.length; i++) {
        var n = dataModel.nodes[i];
        if (nodeIds.indexOf(n.id) >= 0) {

            // Create field value in node if needed
            if (!n.hasOwnProperty("value")) {
                n.value = [];
            }

            var displayAttr = provider$1.node.getReturnAttributes(n.label)[0];

            // Find data for this node and add value
            n.data.forEach(function (d) {
                if (d.hasOwnProperty(displayAttr) && d[displayAttr] === displayAttributeValue) {
                    isAnyChangeDone = true;
                    n.value.push({attributes: d});
                }
            });
        }
    }

    if (isAnyChangeDone) {
        result.hasChanged = true;
        graph$1.hasGraphChanged = true;
        update();
    }
};

/**
 * Remove a value from a node.
 * If the value is not found nothing is done.
 *
 * @param n
 * @param value
 */
node.removeValue = function (n, value) {
    var isAnyChangeDone = false;

    node.collapseNode(n);

    for (var j = n.value.length - 1; j >= 0; j--) {
        if (n.value[j] === value) {
            n.value.splice(j, 1);

            isAnyChangeDone = true;
        }
    }
    return isAnyChangeDone;
};

node.removeValues = function (n) {
    var isAnyChangeDone = false;

    node.collapseNode(n);

    if (n.value !== undefined && n.value.length > 0) {
        n.value.length = 0;
        isAnyChangeDone = true;
    }

    return isAnyChangeDone
};

/**
 * Get the value in the provided nodeId for a specific value id.
 *
 * @param nodeId
 * @param constraintAttributeValue
 */
node.getValue = function (nodeId, constraintAttributeValue) {
    for (var i = 0; i < dataModel.nodes.length; i++) {
        var n = dataModel.nodes[i];

        if (n.id === nodeId) {
            var constraintAttribute = provider$1.node.getConstraintAttribute(n.label);

            for (var j = n.value.length - 1; j >= 0; j--) {
                if (n.value[j].attributes[constraintAttribute] === constraintAttributeValue) {
                    return n.value[j]
                }
            }
        }
    }
};

/**
 * Remove in all expanded nodes the value containing the specified value for the given attribute.
 * And move it back to nodes data.
 *
 * @param attribute
 * @param value
 */
node.removeExpandedValue = function (attribute, value) {
    var isAnyChangeDone = false;

    // For each expanded nodes in reverse order as some values can be removed
    for (var i = dataModel.nodes.length - 1; i >= 0; i--) {
        if (dataModel.nodes[i].valueExpanded) {

            var removedValues = [];

            // Remove values
            for (var j = dataModel.nodes[i].value.length - 1; j >= 0; j--) {
                if (dataModel.nodes[i].value[j].attributes[attribute] === value) {
                    isAnyChangeDone = true;

                    removedValues = removedValues.concat(dataModel.nodes[i].value.splice(j, 1));
                }
            }

            //And add them back in data
            for (var k = 0; k < removedValues.length; k++) {
                dataModel.nodes[i].data.push(removedValues[k].attributes);
            }

            // Refresh node
            node.collapseNode(dataModel.nodes[i]);
            node.expandNode(dataModel.nodes[i]);
        }
    }

    if (isAnyChangeDone) {
        result.hasChanged = true;
        graph$1.hasGraphChanged = true;
        update();
    }
};

/**
 * Return all nodes with isAutoLoadValue property set to true.
 */
node.getAutoLoadValueNodes = function () {
    return dataModel.nodes
        .filter(function (d) {
            return d.hasOwnProperty("isAutoLoadValue") && d.isAutoLoadValue === true && !(d.isNegative === true);
        });
};

/**
 * Add a list of related value if not already found in node.
 * A value is defined with the following structure
 * {
 *   id,
 *   rel,
 *   label
 * }
 *
 * @param n
 * @param values
 * @param isNegative
 */
node.addRelatedValues = function (n, values, isNegative) {
    var valuesToAdd = node.filterExistingValues(n, values);

    if (valuesToAdd.length <= 0) {
        return;
    }

    var statements = [];

    valuesToAdd.forEach(function (v) {
        var constraintAttr = provider$1.node.getConstraintAttribute(v.label);

        var statement = "MATCH ";
        if (constraintAttr === query.NEO4J_INTERNAL_ID) {
            statement += "(v:`" + v.label + "`) WHERE (ID(v) = $p)";
        } else {
            statement += "(v:`" + v.label + "`) WHERE (v." + constraintAttr + " = $p)";
        }

        var resultAttributes = provider$1.node.getReturnAttributes(v.label);
        var sep = "";

        statement += " RETURN DISTINCT \"" + v.rel + "\" AS rel, \"" + v.label + "\" AS label, {" + resultAttributes.reduce(function (a, attr) {
            a += sep + attr + ":v." + attr;
            sep = ", ";
            return a
        }, "") + "} AS value LIMIT 1";

        statements.push(
            {
                "statement": statement,
                "parameters": {p: v.id},
                "resultDataContents": ["row"]
            }
        );
    });

    logger.info("addRelatedValues ==>");
    runner.run(
        {
            "statements": statements
        })
        .then(function (results) {
            logger.info("<== addRelatedValues");

            var parsedData = runner.toObject(results);
            var count = 0;
            parsedData.forEach(function (data) {
                if (data.length > 0) {
                    var dataLabel = data[0].label;
                    var dataValue = data[0].value;
                    var dataRel = data[0].rel;

                    var value = {
                        "id": dataModel.generateId(),
                        "parent": n,
                        "attributes": dataValue,
                        "type": node.NodeTypes.VALUE,
                        "label": dataLabel
                    };
                    graph$1.ignoreCount = true;

                    var nodeRelationships = n.relationships;
                    var nodeRels = nodeRelationships.filter(function (r) {
                        return r.label === dataRel && r.target === dataLabel
                    });

                    var nodeRel = {label: dataRel, target: dataLabel};
                    if (nodeRels.length > 0) {
                        nodeRel = nodeRels[0];
                    }

                    graph$1.addRelationshipData(n, nodeRel, function () {
                        count++;

                        if (count === parsedData.length) {
                            graph$1.ignoreCount = false;
                            graph$1.hasGraphChanged = true;
                            result.hasChanged = true;
                            update();
                        }
                    }, [value], isNegative);
                }
            });
        })
        .catch(function (error) {
            logger.error(error);
        });
};

/**
 * Add a list of related value prefixed by a path of nodes.
 * A value is defined with the following structure
 * {
 *   id,
 *   rel,
 *   label
 * }
 *
 * @param n
 * @param relPath
 * @param values
 * @param isNegative
 */
node.addRelatedBranch = function (n, relPath, values, isNegative) {
    if (relPath.length > 0) {
        var rel = relPath[0];
        relPath = relPath.slice(1);

        var relationships = n.relationships.filter(function (r) {
            return r.label === rel.type && r.target === rel.target;
        });

        if (relationships.length > 0) {
            graph$1.addRelationshipData(n, relationships[0], function (createdNode) {
                node.addRelatedBranch(createdNode, relPath, values, isNegative);
            });
        }
    } else {
        node.addRelatedValues(n, values, isNegative);
    }
};

/**
 * A value is defined with the following structure
 * {
 *   id,
 *   rel,
 *   label
 * }
 *
 * @param n
 * @param values
 */
node.filterExistingValues = function (n, values) {
    var notFoundValues = [];
    var possibleValueNodes = dataModel.nodes.filter(function (n) {
        return n.parent === n && n.hasOwnProperty("value") && n.value.length > 0;
    });

    values.forEach(function (v) {
        var found = false;
        var constraintAttr = provider$1.node.getConstraintAttribute(v.label);

        possibleValueNodes.forEach(function (n) {
            if (n.label === v.label) {
                n.value.forEach(function (nv) {
                    if (nv.attributes[constraintAttr] === v.id) {
                        found = true;
                    }
                });
            }
        });

        if (!found) {
            notFoundValues.push(v);
        }
    });

    return notFoundValues;
};

/**
 * Function called to expand a node containing values.
 * This function will create the value nodes with the clicked node internal data.
 * Only nodes corresponding to the current page index will be generated.
 *
 * @param clickedNode
 */
node.expandNode = function (clickedNode) {

    graph$1.notifyListeners(graph$1.Events.GRAPH_NODE_VALUE_EXPAND, [clickedNode]);

    // Get subset of node corresponding to the current node page and page size
    var lIndex = clickedNode.page * node.PAGE_SIZE;
    var sIndex = lIndex - node.PAGE_SIZE;

    var dataToAdd = clickedNode.data.slice(sIndex, lIndex);
    var parentAngle = graph$1.computeParentAngle(clickedNode);

    // Then each node are created and dispatched around the clicked node using computed coordinates.
    var i = 1;
    dataToAdd.forEach(function (d) {
        var angleDeg;
        if (clickedNode.parent) {
            angleDeg = (((360 / (dataToAdd.length + 1)) * i));
        } else {
            angleDeg = (((360 / (dataToAdd.length)) * i));
        }

        var nx = clickedNode.x + (100 * Math.cos((angleDeg * (Math.PI / 180)) - parentAngle)),
            ny = clickedNode.y + (100 * Math.sin((angleDeg * (Math.PI / 180)) - parentAngle));

        var n = {
            "id": dataModel.generateId(),
            "parent": clickedNode,
            "attributes": d,
            "type": node.NodeTypes.VALUE,
            "label": clickedNode.label,
            "count": d.count,
            "x": nx,
            "y": ny,
            "internalID": d[query.NEO4J_INTERNAL_ID.queryInternalName]
        };

        dataModel.nodes.push(n);
        dataModel.links.push(
            {
                id: "l" + dataModel.generateId(),
                source: clickedNode,
                target: n,
                type: graph$1.link.LinkTypes.VALUE
            }
        );

        i++;
    });

    // Pin clicked node and its parent to avoid the graph to move for selection, only new value nodes will blossom around the clicked node.
    clickedNode.fixed = true;
    clickedNode.fx = clickedNode.x;
    clickedNode.fy = clickedNode.y;
    if (clickedNode.parent && clickedNode.parent.type !== node.NodeTypes.ROOT) {
        clickedNode.parent.fixed = true;
        clickedNode.parent.fx = clickedNode.parent.x;
        clickedNode.parent.fy = clickedNode.parent.y;
    }
    // Change node state
    clickedNode.valueExpanded = true;
    update();
};

/**
 * Fetches the list of relationships of a node and store them in the relationships property.
 *
 * @param n the node to fetch the relationships.
 * @param callback
 * @param directionAngle
 */
node.loadRelationshipData = function (n, callback, directionAngle) {
    var schema = provider$1.node.getSchema(n.label);

    if (schema !== undefined) {
        if (schema.hasOwnProperty("rel") && schema.rel.length > 0) {
            callback(node.pie.startAngle(directionAngle - Math.PI).endAngle(directionAngle + Math.PI)(schema.rel).map(function (d) {
                var data = {
                    id: d.data.label + d.data.target.label,
                    label: d.data.label,
                    target: d.data.target.label,
                    count: 0,
                    startAngle: d.startAngle,
                    endAngle: d.endAngle,
                    directionAngle: (d.startAngle + d.endAngle) / 2
                };

                if (d.data.isReverse === true) {
                    data.isReverse = true;
                }

                return data;
            }));
        } else {
            callback([]);
        }
    } else {
        var nodeRelationQuery = query.generateNodeRelationQuery(n);

        logger.info("Relations (" + n.label + ") ==>");
        runner.run(
            {
                "statements": [
                    {
                        "statement": nodeRelationQuery.statement,
                        "parameters": nodeRelationQuery.parameters
                    }]
            })
            .then(function (results) {
                logger.info("<== Relations (" + n.label + ")");
                var parsedData = runner.toObject(results);

                // Filter data to eventually remove relations if a filter has been defined in config.
                var filteredData = parsedData[0].filter(function (d) {
                    return query.filterRelation(d);
                });

                filteredData = node.pie.startAngle(directionAngle - Math.PI).endAngle(directionAngle + Math.PI)(filteredData).map(function (d) {
                    return {
                        id: d.data.label + d.data.target,
                        label: d.data.label,
                        target: d.data.target,
                        count: d.data.count.toString(),
                        startAngle: d.startAngle,
                        endAngle: d.endAngle,
                        directionAngle: (d.startAngle + d.endAngle) / 2
                    }
                });

                callback(filteredData);
            })
            .catch(function (error) {
                logger.error(error);
                callback([]);
            });
    }
};

/**
 * Expands all the relationships available in node.
 *
 * @param n
 * @param callback
 */
node.expandRelationships = function (n, callback) {
    var callbackCount = 0;

    if (n.hasOwnProperty("relationships") && n.relationships.length > 0) {

        for (var i = 0; i < n.relationships.length; i++) {
            graph$1.addRelationshipData(n, n.relationships[i], function () {
                callbackCount++;

                if (callbackCount === n.relationships.length) {
                    callback();
                }
            });
        }
    } else {
        callback();
    }
};

/**
 * Remove a node and its relationships (recursively) from the graph.
 *
 * @param n the node to remove.
 */
node.removeNode = function (n) {
    var willChangeResults = n.hasOwnProperty("value") && n.value.length > 0;

    var linksToRemove = dataModel.links.filter(function (l) {
        return l.source === n;
    });

    // Remove children nodes from model
    linksToRemove.forEach(function (l) {
        var rc = node.removeNode(l.target);
        willChangeResults = willChangeResults || rc;
    });

    // Remove links to nodes from model
    for (var i = dataModel.links.length - 1; i >= 0; i--) {
        if (dataModel.links[i].target === n) {
            dataModel.links.splice(i, 1);
        }
    }

    dataModel.nodes.splice(dataModel.nodes.indexOf(n), 1);

    return willChangeResults;
};

/**
 * Remove empty branches containing a node.
 *
 * @param n the node to remove.
 * @return true if node have been removed
 */
node.removeEmptyBranches = function (n) {
    var hasValues = n.hasOwnProperty("value") && n.value.length > 0;

    var childrenLinks = dataModel.links.filter(function (l) {
        return l.source === n;
    });

    childrenLinks.forEach(function (l) {
        var hasRemainingNodes = !node.removeEmptyBranches(l.target);
        hasValues = hasValues || hasRemainingNodes;
    });

    if (!hasValues) {
        // Remove links to node from model
        for (var i = dataModel.links.length - 1; i >= 0; i--) {
            if (dataModel.links[i].target === n) {
                dataModel.links.splice(i, 1);
            }
        }

        dataModel.nodes.splice(dataModel.nodes.indexOf(n), 1);
    }

    return !hasValues;
};

/**
 * Get in the parent nodes the closest one to the root.
 *
 * @param n the node to start from.
 * @return {*} the trunk node or the node in parameters if not found.
 */
node.getTrunkNode = function (n) {

    for (var i = 0; i < dataModel.links.length; i++) {
        var l = dataModel.links[i];
        if (l.target === n) {
            if (l.source !== graph$1.getRootNode()) {
                return node.getTrunkNode(l.source);
            }
        }
    }

    return n;
};

/**
 * Function to add on node event to clear the selection.
 * Call to this function on a node will remove the selected value and trigger a graph update.
 */
node.clearSelection = function () {
    // Prevent default event like right click  opening menu.
    d3__namespace.event.preventDefault();

    // Get clicked node.
    var clickedNode = d3__namespace.select(this).data()[0];

    // Collapse all expanded choose nodes first
    node.collapseAllNode();

    if (clickedNode.value !== undefined && clickedNode.value.length > 0 && !clickedNode.immutable) {
        // Remove last value of chosen node
        clickedNode.value.pop();

        if (clickedNode.isNegative === true) {
            if (clickedNode.value.length === 0) {
                // Remove all related nodes
                for (var i = dataModel.links.length - 1; i >= 0; i--) {
                    if (dataModel.links[i].source === clickedNode) {
                        node.removeNode(dataModel.links[i].target);
                    }
                }

                clickedNode.count = 0;
            }
        }

        result.hasChanged = true;
        graph$1.hasGraphChanged = true;
        update();
    }
};

var graph = {};

graph.link = link;
graph.node = node;

graph.DISABLE_RELATION = false;
graph.DISABLE_COUNT = false;

/**
 * ID of the HTML component where the graph query builder elements will be generated in.
 * @type {string}
 */
graph.containerId = "popoto-graph";
graph.hasGraphChanged = true;
// Defines the min and max level of zoom available in graph query builder.
graph.zoom = d3__namespace.zoom().scaleExtent([0.1, 10]);
graph.WHEEL_ZOOM_ENABLED = true;
graph.USE_DONUT_FORCE = false;
graph.USE_VORONOI_LAYOUT = false;
graph.USE_FIT_TEXT = false;

/**
 * Define the list of listenable events on graph.
 */
graph.Events = Object.freeze({
    NODE_ROOT_ADD: "root.node.add",
    NODE_EXPAND_RELATIONSHIP: "node.expandRelationship",
    GRAPH_SAVE: "graph.save",
    GRAPH_RESET: "graph.reset",
    GRAPH_NODE_RELATION_ADD: "graph.node.relation_add",
    GRAPH_NODE_VALUE_EXPAND: "graph.node.value_expand",
    GRAPH_NODE_VALUE_COLLAPSE: "graph.node.value_collapse",
    GRAPH_NODE_ADD_VALUE: "graph.node.add_value",
    GRAPH_NODE_DATA_LOADED: "graph.node.data_loaded"
});

graph.listeners = {};

/**
 * Add a listener to the specified event.
 *
 * @param event name of the event to add the listener.
 * @param listener the listener to add.
 */
graph.on = function (event, listener) {
    if (!graph.listeners.hasOwnProperty(event)) {
        graph.listeners[event] = [];
    }

    graph.listeners[event].push(listener);
};

/**
 * Notify the listeners.
 *
 * @param event
 * @param parametersArray
 */
graph.notifyListeners = function (event, parametersArray) {
    if (graph.listeners.hasOwnProperty(event)) {
        graph.listeners[event].forEach(function (listener) {
            listener.apply(event, parametersArray);
        });
    }
};

/**
 * Add a listener on graph save event.
 * @param listener
 */
graph.onSave = function (listener) {
    graph.on(graph.Events.GRAPH_SAVE, listener);
};

/**
 * Add a listener on graph reset event.
 * @param listener
 */
graph.onReset = function (listener) {
    graph.on(graph.Events.GRAPH_RESET, listener);
};

/**
 * Set default graph to a predefined value.
 * @param graph
 */
graph.setDefaultGraph = function (graph) {
    graph.mainLabel = graph;
};

/**
 * Generates all the HTML and SVG element needed to display the graph query builder.
 * Everything will be generated in the container with id defined by graph.containerId.
 */
graph.createGraphArea = function () {

    var htmlContainer = d3__namespace.select("#" + graph.containerId);

    toolbar.render(htmlContainer);

    graph.svgTag = htmlContainer.append("svg")
        .attr("class", "ppt-svg-graph")
        // .attr("viewBox", "0 0 800 600") TODO to avoid need of windows resize event
        .call(graph.zoom.on("zoom", graph.rescale));

    graph.svgTag.on("dblclick.zoom", null);

    if (!graph.WHEEL_ZOOM_ENABLED) {
        // Disable mouse wheel events.
        graph.svgTag.on("wheel.zoom", null)
            .on("mousewheel.zoom", null);
    }

    // Objects created inside a <defs> element are not rendered immediately; instead, think of them as templates or macros created for future use.
    graph.svgdefs = graph.svgTag.append("defs");

    // Cross marker for path with id #cross -X-
    graph.svgdefs.append("marker")
        .attr("id", "cross")
        .attr("refX", 10)
        .attr("refY", 10)
        .attr("markerWidth", 20)
        .attr("markerHeight", 20)
        .attr("markerUnits", "strokeWidth")
        .attr("orient", "auto")
        .append("path")
        .attr("class", "ppt-marker-cross")
        .attr("d", "M5,5 L15,15 M15,5 L5,15");

    // Triangle marker for paths with id #arrow --|>
    graph.svgdefs.append("marker")
        .attr("id", "arrow")
        .attr("refX", 9)
        .attr("refY", 3)
        .attr("markerWidth", 10)
        .attr("markerHeight", 10)
        .attr("markerUnits", "strokeWidth")
        .attr("orient", "auto")
        .append("path")
        .attr("class", "ppt-marker-arrow")
        .attr("d", "M0,0 L0,6 L9,3 z");

    // Reversed triangle marker for paths with id #reverse-arrow <|--
    graph.svgdefs.append("marker")
        .attr("id", "reverse-arrow")
        .attr("refX", 0)
        .attr("refY", 3)
        .attr("markerWidth", 10)
        .attr("markerHeight", 10)
        .attr("markerUnits", "strokeWidth")
        .attr("orient", "auto")
        .append("path")
        .attr("class", "ppt-marker-reverse-arrow")
        .attr("d", "M0,3 L9,6 L9,0 z");

    // Gray scale filter for images.
    var grayscaleFilter = graph.svgdefs.append("filter")
        .attr("id", "grayscale");

    grayscaleFilter.append("feColorMatrix")
        .attr("type", "saturate")
        .attr("values", "0");

    // to change brightness
    // var feCT = grayscaleFilter
    //     .append("feComponentTransfer");
    //
    // feCT.append("feFuncR")
    //     .attr("type", "linear")
    //     .attr("slope", "0.2");
    //
    // feCT.append("feFuncG")
    //     .attr("type", "linear")
    //     .attr("slope", "0.2");
    //
    // feCT.append("feFuncB")
    //     .attr("type", "linear")
    //     .attr("slope", "0.2");

    // gooey
    var filter = graph.svgdefs.append("filter").attr("id", "gooey");
    filter.append("feGaussianBlur")
        .attr("in", "SourceGraphic")
        .attr("stdDeviation", "10")
        //to fix safari: http://stackoverflow.com/questions/24295043/svg-gaussian-blur-in-safari-unexpectedly-lightens-image
        .attr("color-interpolation-filters", "sRGB")
        .attr("result", "blur");
    filter.append("feColorMatrix")
        .attr("class", "blurValues")
        .attr("in", "blur")
        .attr("mode", "matrix")
        .attr("values", "1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 35 -6")
        .attr("result", "gooey");
    filter.append("feBlend")
        .attr("in", "SourceGraphic")
        .attr("in2", "gooey")
        .attr("operator", "atop");


    graph.svgdefs.append("g")
        .attr("id", "voronoi-clip-path");

    graph.svg = graph.svgTag.append("svg:g");

    // Create two separated area for links and nodes
    // Links and nodes are separated in a dedicated "g" element
    // and nodes are generated after links to ensure that nodes are always on foreground.
    graph.svg.append("g").attr("id", graph.link.gID);
    graph.svg.append("g").attr("id", graph.node.gID);

    // This listener is used to center the root node in graph during a window resize.
    // TODO can the listener be limited on the parent container only?
    window.addEventListener('resize', graph.centerRootNode);
};

graph.getRootNode = function () {
    return dataModel.getRootNode();
};

graph.centerRootNode = function () {
    dataModel.getRootNode().fx = graph.getSVGWidth() / 2;
    dataModel.getRootNode().fy = graph.getSVGHeight() / 2;
    update();
};

/**
 * Get the actual width of the SVG element containing the graph query builder.
 * @returns {number}
 */
graph.getSVGWidth = function () {
    if (typeof graph.svg == 'undefined' || graph.svg.empty()) {
        logger.debug("graph.svg is undefined or empty.");
        return 0;
    } else {
        return document.getElementById(graph.containerId).clientWidth;
    }
};

/**
 * Get the actual height of the SVG element containing the graph query builder.
 * @returns {number}
 */
graph.getSVGHeight = function () {
    if (typeof graph.svg == 'undefined' || graph.svg.empty()) {
        logger.debug("graph.svg is undefined or empty.");
        return 0;
    } else {
        return document.getElementById(graph.containerId).clientHeight;
    }
};

/**
 * Function to call on SVG zoom event to update the svg transform attribute.
 */
graph.rescale = function () {
    var transform = d3__namespace.event.transform;
    if (isNaN(transform.x) || isNaN(transform.y) || isNaN(transform.k)) {
        graph.svg.attr("transform", d3__namespace.zoomIdentity);
    } else {
        graph.svg.attr("transform", transform);
    }
};

graph.CHARGE = -500;

/**
 *  Create the D3.js force simultation for the graph query builder.
 */
// TODO ZZZ rename to create createForceSimulation
graph.createForceLayout = function () {

    graph.force = d3__namespace.forceSimulation()
        .force("charge", d3__namespace.forceManyBody()
            .strength(function (d) {
                    if (d.charge) {
                        return d.charge;
                    } else {
                        return graph.CHARGE;
                    }
                }
            )
        )
        .force(
            "link",
            d3__namespace.forceLink().id(
                function (d) {
                    return d.id;
                }
            ).distance(provider$1.link.getDistance)
        );


    graph.force.nodes(dataModel.nodes);
    graph.force.force("link").links(dataModel.links);

    graph.force.on("tick", graph.tick);
};

/**
 * Adds graph root nodes using the label set as parameter.
 * All the other nodes should have been removed first to avoid inconsistent data.
 *
 * @param label label of the node to add as root.
 */
graph.addRootNode = function (label) {
    if (dataModel.nodes.length > 0) {
        logger.warn("graph.addRootNode is called but the graph is not empty.");
    }
    if (provider$1.node.getSchema(label) !== undefined) {
        graph.addSchema(provider$1.node.getSchema(label));
    } else {

        var n = {
            "id": dataModel.generateId(),
            "type": graph.node.NodeTypes.ROOT,
            // x and y coordinates are set to the center of the SVG area.
            // These coordinate will never change at runtime except if the window is resized.
            "x": graph.getSVGWidth() / 2,
            "y": graph.getSVGHeight() / 2,
            "fx": graph.getSVGWidth() / 2,
            "fy": graph.getSVGHeight() / 2,
            "tx": graph.getSVGWidth() / 2,
            "ty": graph.getSVGHeight() / 2,
            "label": label,
            // The node is fixed to always remain in the center of the svg area.
            // This property should not be changed at runtime to avoid issues with the zoom and pan.
            "fixed": true,
            // Label used internally to identify the node.
            // This label is used for example as cypher query identifier.
            "internalLabel": graph.node.generateInternalLabel(label),
            // List of relationships
            "relationships": [],
            "isAutoLoadValue": provider$1.node.getIsAutoLoadValue(label) === true
        };

        dataModel.nodes.push(n);
        graph.notifyListeners(graph.Events.NODE_ROOT_ADD, [n]);

        graph.node.loadRelationshipData(n, function (relationships) {
            n.relationships = relationships;

            if (provider$1.node.getIsAutoExpandRelations(label)) {

                graph.ignoreCount = true;
                graph.node.expandRelationships(n, function () {

                    graph.ignoreCount = false;
                    graph.hasGraphChanged = true;
                    updateGraph();
                });
            } else {
                graph.hasGraphChanged = true;
                updateGraph();
            }
        }, Math.PI / 2);
    }
};

graph.loadSchema = function (graphToLoad) {
    if (dataModel.nodes.length > 0) {
        logger.warn("graph.loadSchema is called but the graph is not empty.");
    }

    var rootNodeSchema = graphToLoad;
    var rootNode = {
        "id": dataModel.generateId(),
        "type": graph.node.NodeTypes.ROOT,
        // x and y coordinates are set to the center of the SVG area.
        // These coordinate will never change at runtime except if the window is resized.
        "x": graph.getSVGWidth() / 2,
        "y": graph.getSVGHeight() / 2,
        "fx": graph.getSVGWidth() / 2,
        "fy": graph.getSVGHeight() / 2,
        "tx": graph.getSVGWidth() / 2,
        "ty": graph.getSVGHeight() / 2,
        "label": rootNodeSchema.label,
        // The node is fixed to always remain in the center of the svg area.
        // This property should not be changed at runtime to avoid issues with the zoom and pan.
        "fixed": true,
        // Label used internally to identify the node.
        // This label is used for example as cypher query identifier.
        "internalLabel": graph.node.generateInternalLabel(rootNodeSchema.label),
        // List of relationships
        "relationships": [],
        "isAutoLoadValue": provider$1.node.getIsAutoLoadValue(rootNodeSchema.label) === true
    };
    dataModel.nodes.push(rootNode);
    graph.notifyListeners(graph.Events.NODE_ROOT_ADD, [rootNode]);

    var labelSchema = provider$1.node.getSchema(graphToLoad.label);

    // Add relationship from schema if defined
    if (labelSchema !== undefined && labelSchema.hasOwnProperty("rel") && labelSchema.rel.length > 0) {
        var directionAngle = (Math.PI / 2);

        rootNode.relationships = graph.node.pie.startAngle(directionAngle - Math.PI).endAngle(directionAngle + Math.PI)(labelSchema.rel).map(function (d) {

            var data = {
                id: d.data.label + d.data.target.label,
                label: d.data.label,
                target: d.data.target.label,
                count: 0,
                startAngle: d.startAngle,
                endAngle: d.endAngle,
                directionAngle: (d.startAngle + d.endAngle) / 2
            };

            if (d.data.isReverse === true) {
                data.isReverse = true;
            }

            return data;
        });
    } else {
        graph.node.loadRelationshipData(rootNode, function (relationships) {
            rootNode.relationships = relationships;
            graph.hasGraphChanged = true;
            updateGraph();
        }, Math.PI / 2);
    }

    if (rootNodeSchema.hasOwnProperty("value")) {
        var nodeSchemaValue = [].concat(rootNodeSchema.value);
        rootNode.value = [];
        nodeSchemaValue.forEach(function (value) {
            rootNode.value.push(
                {
                    "id": dataModel.generateId(),
                    "parent": rootNode,
                    "attributes": value,
                    "type": graph.node.NodeTypes.VALUE,
                    "label": rootNode.label
                }
            );
        });
    }

    if (rootNodeSchema.hasOwnProperty("rel")) {
        for (var linkIndex = 0; linkIndex < rootNodeSchema.rel.length; linkIndex++) {
            graph.loadSchemaRelation(rootNodeSchema.rel[linkIndex], rootNode, linkIndex + 1, rootNodeSchema.rel.length);
        }
    }
};

graph.loadSchemaRelation = function (relationSchema, parentNode, linkIndex, parentLinkTotalCount) {
    var targetNodeSchema = relationSchema.target;
    var target = graph.loadSchemaNode(targetNodeSchema, parentNode, linkIndex, parentLinkTotalCount, relationSchema.label, (relationSchema.hasOwnProperty("isReverse") && relationSchema.isReverse === true));

    var newLink = {
        id: "l" + dataModel.generateId(),
        source: parentNode,
        target: target,
        type: graph.link.LinkTypes.RELATION,
        label: relationSchema.label,
        schema: relationSchema
    };

    dataModel.links.push(newLink);

    var labelSchema = provider$1.node.getSchema(targetNodeSchema.label);

    // Add relationship from schema if defined
    if (labelSchema !== undefined && labelSchema.hasOwnProperty("rel") && labelSchema.rel.length > 0) {
        var directionAngle = (Math.PI / 2);

        target.relationships = graph.node.pie.startAngle(directionAngle - Math.PI).endAngle(directionAngle + Math.PI)(labelSchema.rel).map(function (d) {
            var data = {
                id: d.data.label + d.data.target.label,
                label: d.data.label,
                target: d.data.target.label,
                count: 0,
                startAngle: d.startAngle,
                endAngle: d.endAngle,
                directionAngle: (d.startAngle + d.endAngle) / 2
            };

            if (d.data.isReverse === true) {
                data.isReverse = true;
            }

            return data;
        });
    } else {
        graph.node.loadRelationshipData(target, function (relationships) {
            target.relationships = relationships;
            graph.hasGraphChanged = true;
            updateGraph();
        }, Math.PI / 2);
    }

    if (targetNodeSchema.hasOwnProperty("rel")) {
        for (var linkIndex2 = 0; linkIndex2 < targetNodeSchema.rel.length; linkIndex2++) {
            graph.loadSchemaRelation(targetNodeSchema.rel[linkIndex2], target, linkIndex2 + 1, targetNodeSchema.rel.length);
        }
    }
};

graph.loadSchemaNode = function (nodeSchema, parentNode, index, parentLinkTotalCount, parentRel, isReverse) {
    var isGroupNode = provider$1.node.getIsGroup(nodeSchema);
    var parentAngle = graph.computeParentAngle(parentNode);

    var angleDeg;
    if (parentAngle) {
        angleDeg = (((360 / (parentLinkTotalCount + 1)) * index));
    } else {
        angleDeg = (((360 / (parentLinkTotalCount)) * index));
    }

    var nx = parentNode.x + (200 * Math.cos((angleDeg * (Math.PI / 180)) - parentAngle)),
        ny = parentNode.y + (200 * Math.sin((angleDeg * (Math.PI / 180)) - parentAngle));

    // TODO add force coordinate X X X
    // var tx = nx + ((provider.link.getDistance(newLink)) * Math.cos(link.directionAngle - Math.PI / 2));
    // var ty = ny + ((provider.link.getDistance(newLink)) * Math.sin(link.directionAngle - Math.PI / 2));

    var n = {
        "id": dataModel.generateId(),
        "parent": parentNode,
        "parentRel": parentRel,
        "type": (isGroupNode) ? graph.node.NodeTypes.GROUP : graph.node.NodeTypes.CHOOSE,
        "label": nodeSchema.label,
        "fixed": false,
        "internalLabel": graph.node.generateInternalLabel(nodeSchema.label),
        "x": nx,
        "y": ny,
        "schema": nodeSchema,
        "isAutoLoadValue": provider$1.node.getIsAutoLoadValue(nodeSchema.label) === true,
        "relationships": []
    };

    if (isReverse === true) {
        n.isParentRelReverse = true;
    }

    if (nodeSchema.hasOwnProperty("isNegative") && nodeSchema.isNegative === true) {
        n.isNegative = true;
        n.count = 0;
    }

    dataModel.nodes.push(n);

    if (nodeSchema.hasOwnProperty("value")) {
        var nodeSchemaValue = [].concat(nodeSchema.value);
        n.value = [];
        nodeSchemaValue.forEach(function (value) {
            n.value.push(
                {
                    "id": dataModel.generateId(),
                    "parent": n,
                    "attributes": value,
                    "type": graph.node.NodeTypes.VALUE,
                    "label": n.label
                }
            );
        });
    }

    return n;
};

/**
 * Adds a complete graph from schema.
 * All the other nodes should have been removed first to avoid inconsistent data.
 *
 * @param graphSchema schema of the graph to add.
 */
graph.addSchema = function (graphSchema) {
    if (dataModel.nodes.length > 0) {
        logger.warn("graph.addSchema is called but the graph is not empty.");
    }

    var rootNodeSchema = graphSchema;

    var rootNode = {
        "id": dataModel.generateId(),
        "type": graph.node.NodeTypes.ROOT,
        // x and y coordinates are set to the center of the SVG area.
        // These coordinate will never change at runtime except if the window is resized.
        "x": graph.getSVGWidth() / 2,
        "y": graph.getSVGHeight() / 2,
        "fx": graph.getSVGWidth() / 2,
        "fy": graph.getSVGHeight() / 2,
        "tx": graph.getSVGWidth() / 2,
        "ty": graph.getSVGHeight() / 2,
        "label": rootNodeSchema.label,
        // The node is fixed to always remain in the center of the svg area.
        // This property should not be changed at runtime to avoid issues with the zoom and pan.
        "fixed": true,
        // Label used internally to identify the node.
        // This label is used for example as cypher query identifier.
        "internalLabel": graph.node.generateInternalLabel(rootNodeSchema.label),
        // List of relationships
        "relationships": [],
        "isAutoLoadValue": provider$1.node.getIsAutoLoadValue(rootNodeSchema.label) === true
    };
    dataModel.nodes.push(rootNode);
    graph.notifyListeners(graph.Events.NODE_ROOT_ADD, [rootNode]);

    if (rootNodeSchema.hasOwnProperty("rel") && rootNodeSchema.rel.length > 0) {
        var directionAngle = (Math.PI / 2);

        rootNode.relationships = graph.node.pie.startAngle(directionAngle - Math.PI).endAngle(directionAngle + Math.PI)(rootNodeSchema.rel).map(function (d) {
            var data = {
                id: d.data.label + d.data.target.label,
                label: d.data.label,
                target: d.data.target.label,
                count: 0,
                startAngle: d.startAngle,
                endAngle: d.endAngle,
                directionAngle: (d.startAngle + d.endAngle) / 2
            };

            if (d.data.isReverse === true) {
                data.isReverse = true;
            }

            return data;
        });
    }

    // if (!isCollapsed && rootNodeSchema.hasOwnProperty("rel")) {
    //     for (var linkIndex = 0; linkIndex < rootNodeSchema.rel.length; linkIndex++) {
    //         graph.addSchemaRelation(rootNodeSchema.rel[linkIndex], rootNode, linkIndex + 1, rootNodeSchema.rel.length);
    //     }
    // }
};

graph.addSchemaRelation = function (relationSchema, parentNode, linkIndex, parentLinkTotalCount) {
    var targetNodeSchema = relationSchema.target;
    var target = graph.addSchemaNode(targetNodeSchema, parentNode, linkIndex, parentLinkTotalCount, relationSchema.label);

    var newLink = {
        id: "l" + dataModel.generateId(),
        source: parentNode,
        target: target,
        type: graph.link.LinkTypes.RELATION,
        label: relationSchema.label,
        schema: relationSchema
    };

    dataModel.links.push(newLink);
};

graph.addSchemaNode = function (nodeSchema, parentNode, index, parentLinkTotalCount, parentRel) {
    var isGroupNode = provider$1.node.getIsGroup(nodeSchema);
    var isCollapsed = nodeSchema.hasOwnProperty("collapsed") && nodeSchema.collapsed === true;

    var parentAngle = graph.computeParentAngle(parentNode);

    var angleDeg;
    if (parentAngle) {
        angleDeg = (((360 / (parentLinkTotalCount + 1)) * index));
    } else {
        angleDeg = (((360 / (parentLinkTotalCount)) * index));
    }

    var nx = parentNode.x + (200 * Math.cos((angleDeg * (Math.PI / 180)) - parentAngle)),
        ny = parentNode.y + (200 * Math.sin((angleDeg * (Math.PI / 180)) - parentAngle));

    // TODO add force coordinate X X X
    // var tx = nx + ((provider.link.getDistance(newLink)) * Math.cos(link.directionAngle - Math.PI / 2));
    // var ty = ny + ((provider.link.getDistance(newLink)) * Math.sin(link.directionAngle - Math.PI / 2));

    var n = {
        "id": dataModel.generateId(),
        "parent": parentNode,
        "parentRel": parentRel,
        "type": (isGroupNode) ? graph.node.NodeTypes.GROUP : graph.node.NodeTypes.CHOOSE,
        "label": nodeSchema.label,
        "fixed": false,
        "internalLabel": graph.node.generateInternalLabel(nodeSchema.label),
        "x": nx,
        "y": ny,
        "schema": nodeSchema,
        "isAutoLoadValue": provider$1.node.getIsAutoLoadValue(nodeSchema.label) === true,
        "relationships": []
    };

    if (nodeSchema.hasOwnProperty("rel") && nodeSchema.rel.length > 0) {
        var relMap = {};
        var relSegments = [];

        for (var i = 0; i < nodeSchema.rel.length; i++) {
            var rel = nodeSchema.rel[i];
            var id = rel.label + rel.target.label;

            if (!relMap.hasOwnProperty(id)) {
                relMap[id] = rel;
                relSegments.push(rel);
            }

        }

        n.relationships = graph.node.pie(relSegments).map(function (d) {
            return {
                id: d.data.label + d.data.target.label,
                count: d.data.count || 0,
                label: d.data.label,
                target: d.data.target.label,
                startAngle: d.startAngle,
                endAngle: d.endAngle,
                directionAngle: (d.startAngle + d.endAngle) / 2
            }
        });

    }

    if (nodeSchema.hasOwnProperty("value")) {
        var nodeSchemaValue = [].concat(nodeSchema.value);
        n.value = [];
        nodeSchemaValue.forEach(function (value) {
            n.value.push(
                {
                    "id": dataModel.generateId(),
                    "parent": n,
                    "attributes": value,
                    "type": graph.node.NodeTypes.VALUE,
                    "label": n.label
                }
            );
        });
    }

    dataModel.nodes.push(n);

    if (!isCollapsed && nodeSchema.hasOwnProperty("rel")) {
        for (var linkIndex = 0; linkIndex < nodeSchema.rel.length; linkIndex++) {
            graph.addSchemaRelation(nodeSchema.rel[linkIndex], n, linkIndex + 1, nodeSchema.rel.length);
        }
    }

    return n;
};

/**
 * Get the current schema of the graph.
 * @returns {{}}
 */
graph.getSchema = function () {
    var nodesMap = {};

    var rootNode = dataModel.getRootNode();

    nodesMap[rootNode.id] = {
        label: rootNode.label
    };

    if (rootNode.hasOwnProperty("value")) {
        nodesMap[rootNode.id].value = [];
        rootNode.value.forEach(function (value) {
            nodesMap[rootNode.id].value.push(value.attributes);
        });
    }

    var links = dataModel.links;
    if (links.length > 0) {
        links.forEach(function (l) {
            if (l.type === graph.link.LinkTypes.RELATION) {
                var sourceNode = l.source;
                var targetNode = l.target;

                if (!nodesMap.hasOwnProperty(sourceNode.id)) {
                    nodesMap[sourceNode.id] = {
                        label: sourceNode.label
                    };
                    if (sourceNode.hasOwnProperty("isNegative") && sourceNode.isNegative === true) {
                        nodesMap[sourceNode.id].isNegative = true;
                    }
                    if (sourceNode.hasOwnProperty("value")) {
                        nodesMap[sourceNode.id].value = [];
                        sourceNode.value.forEach(function (value) {
                            nodesMap[sourceNode.id].value.push(value.attributes);
                        });
                    }
                }

                if (!nodesMap.hasOwnProperty(targetNode.id)) {
                    nodesMap[targetNode.id] = {
                        label: targetNode.label
                    };
                    if (targetNode.hasOwnProperty("isNegative") && targetNode.isNegative === true) {
                        nodesMap[targetNode.id].isNegative = true;
                    }
                    if (targetNode.hasOwnProperty("value")) {
                        nodesMap[targetNode.id].value = [];
                        targetNode.value.forEach(function (value) {
                            nodesMap[targetNode.id].value.push(value.attributes);
                        });
                    }
                }

                if (!nodesMap[sourceNode.id].hasOwnProperty("rel")) {
                    nodesMap[sourceNode.id].rel = [];
                }

                var rel = {
                    label: l.label,
                    target: nodesMap[targetNode.id]
                };

                if (targetNode.hasOwnProperty("isParentRelReverse") && targetNode.isParentRelReverse === true) {
                    rel.isReverse = true;
                }

                nodesMap[sourceNode.id].rel.push(rel);
            }
        });

    }

    return nodesMap[rootNode.id];
};

/**
 * Function to call on D3.js force layout tick event.
 * This function will update the position of all links and nodes elements in the graph with the force layout computed coordinate.
 */
graph.tick = function () {
    var paths = graph.svg.selectAll("#" + graph.link.gID + " > g");

    // Update link paths
    paths.selectAll(".ppt-link")
        .attr("d", function (d) {
            var linkSource = d.source;
            var linkTarget = d.target;
            var parentAngle = graph.computeParentAngle(linkTarget);
            var sourceMargin = provider$1.node.getSize(linkSource);
            var targetMargin = provider$1.node.getSize(linkTarget);

            if (!graph.DISABLE_RELATION && linkSource.hasOwnProperty("relationships") && linkSource.relationships.length > 0) {
                sourceMargin = graph.node.getDonutOuterRadius(linkSource);
            }

            if (!graph.DISABLE_RELATION && linkTarget.hasOwnProperty("relationships") && linkTarget.relationships.length > 0) {
                targetMargin = graph.node.getDonutOuterRadius(linkTarget);
            }

            var targetX = linkTarget.x + ((targetMargin) * Math.cos(parentAngle)),
                targetY = linkTarget.y - ((targetMargin) * Math.sin(parentAngle));

            var sourceX = linkSource.x - ((sourceMargin) * Math.cos(parentAngle)),
                sourceY = linkSource.y + ((sourceMargin) * Math.sin(parentAngle));

            // Add an intermediate point in path center
            var middleX = (targetX + sourceX) / 2,
                middleY = (targetY + sourceY) / 2;

            if (linkSource.x <= linkTarget.x || graph.ignoreMirroLinkLabels === true) {
                return "M" + sourceX + " " + sourceY + "L" + middleX + " " + middleY + "L" + targetX + " " + targetY;
            } else {
                return "M" + targetX + " " + targetY + "L" + middleX + " " + middleY + "L" + sourceX + " " + sourceY;
            }
        })
        .attr("marker-end", function (d) {
            if (graph.link.SHOW_MARKER) {
                if (d.target.isParentRelReverse === true) {
                    if (d.source.x > d.target.x) {
                        return "url(#arrow)";
                    }
                } else {
                    if (d.source.x <= d.target.x) {
                        return "url(#arrow)";
                    }
                }
            }
            return null;
        })
        .attr("marker-start", function (d) {
            if (graph.link.SHOW_MARKER) {
                if (d.target.isParentRelReverse === true) {
                    if (d.source.x <= d.target.x) {
                        return "url(#reverse-arrow)";
                    }
                } else {
                    if (d.source.x > d.target.x) {
                        return "url(#reverse-arrow)";
                    }
                }
            }
            return null;
        });

    // Workaround to WebKit browsers:
    // Updating a path element by itself does not trigger redraw on dependent elements that reference this path.
    // So, even though we update the path, the referencing textPath element will not be redrawn.
    // To workaround this update bug, the xlink:href attribute to “#path” is updated.
    paths.selectAll(".ppt-textPath")
        .attr("xlink:href", function (d) {
            return "#ppt-path_" + d.id;
        });

    graph.svg.selectAll("#" + graph.node.gID + " > g")
        .attr("transform", function (d) {
            return "translate(" + (d.x) + "," + (d.y) + ")";
        });

    if (graph.USE_VORONOI_LAYOUT === true) {

        var clip = d3__namespace.select("#voronoi-clip-path").selectAll('.voroclip')
            .data(graph.recenterVoronoi(dataModel.nodes), function (d) {
                return d.point.id;
            });

        clip.enter().append('clipPath')
            .attr('id', function (d) {
                return 'voroclip-' + d.point.id;
            })
            .attr('class', 'voroclip');

        clip.exit().remove();

        clip.selectAll('path').remove();

        clip.append('path')
            .attr('id', function (d) {
                return 'pvoroclip-' + d.point.id;
            })
            .attr('d', function (d) {
                return 'M' + d.join(',') + 'Z';
            });
    }
};

/**
 * Compute the angle in radian between the node and its parent.
 * TODO: clean or add comments to explain the code...
 *
 * @param n node to compute angle.
 * @returns {number} angle in radian.
 */
graph.computeParentAngle = function (n) {
    var angleRadian = 0;
    var r = 100;
    if (n.parent) {
        var xp = n.parent.x;
        var yp = n.parent.y;
        var x0 = n.x;
        var y0 = n.y;
        var dist = Math.sqrt(Math.pow(xp - x0, 2) + Math.pow(yp - y0, 2));

        var k = r / (dist - r);
        var xc = (x0 + (k * xp)) / (1 + k);

        var val = (xc - x0) / r;
        if (val < -1) {
            val = -1;
        }
        if (val > 1) {
            val = 1;
        }

        angleRadian = Math.acos(val);

        if (yp > y0) {
            angleRadian = 2 * Math.PI - angleRadian;
        }
    }
    return angleRadian;
};

/**
 *
 * @param n
 * @param l
 * @param callback
 * @param values
 * @param isNegative
 */
graph.addRelationshipData = function (n, l, callback, values, isNegative) {
    var targetNode = {
        "id": "" + dataModel.generateId(),
        "parent": n,
        "parentRel": l.label,
        "type": graph.node.NodeTypes.CHOOSE,
        "label": l.target,
        "fixed": false,
        "internalLabel": graph.node.generateInternalLabel(l.target),
        "relationships": []
    };

    if (l.isReverse === true) {
        targetNode.isParentRelReverse = true;
    }

    if (values !== undefined && values.length > 0) {
        targetNode.value = values;
    }
    if (isNegative !== undefined && isNegative === true) {
        targetNode.isNegative = true;
    }

    var newLink = {
        id: "l" + dataModel.generateId(),
        source: n,
        target: targetNode,
        type: graph.link.LinkTypes.RELATION,
        label: l.label
    };

    targetNode.x = n.x + ((provider$1.link.getDistance(newLink) * 2 / 3) * Math.cos(l.directionAngle - Math.PI / 2)) + Math.random() * 10;
    targetNode.y = n.y + ((provider$1.link.getDistance(newLink) * 2 / 3) * Math.sin(l.directionAngle - Math.PI / 2)) + Math.random() * 10;

    targetNode.tx = n.tx + ((provider$1.link.getDistance(newLink)) * Math.cos(l.directionAngle - Math.PI / 2));
    targetNode.ty = n.ty + ((provider$1.link.getDistance(newLink)) * Math.sin(l.directionAngle - Math.PI / 2));

    dataModel.nodes.push(targetNode);
    dataModel.links.push(newLink);

    graph.hasGraphChanged = true;
    updateGraph();

    graph.node.loadRelationshipData(targetNode, function (relationships) {
            targetNode.relationships = relationships;

            graph.hasGraphChanged = true;
            updateGraph();

            if (provider$1.node.getIsAutoExpandRelations(targetNode.label)) {
                graph.node.expandRelationships(targetNode, function () {
                    callback(targetNode);
                });
            } else {
                callback(targetNode);
            }
        },
        l.directionAngle
    );


};

graph.voronoi = d3__namespace.voronoi()
    .x(function (d) {
        return d.x;
    })
    .y(function (d) {
        return d.y;
    });

graph.recenterVoronoi = function (nodes) {
    var shapes = [];

    var voronois = graph.voronoi.polygons(nodes.map(function (d) {
        d.x = d.x || 0;
        d.y = d.y || 0;
        return d
    }));

    voronois.forEach(function (d) {
        if (!d.length) {
            return;
        }

        var n = [];
        d.forEach(function (c) {
            n.push([c[0] - d.data.x, c[1] - d.data.y]);
        });

        n.point = d.data;
        shapes.push(n);
    });
    return shapes;
};

var graph$1 = graph;

var provider = {};

/**
 * Default color scale generator.
 * Used in getColor link and node providers.
 */
provider.colorScale = d3__namespace.scaleOrdinal(d3__namespace.schemeCategory10);
provider.link = {};
provider.link.Provider = {};
provider.taxonomy = {};
provider.taxonomy.Provider = {};
provider.node = {};
provider.node.Provider = {};

//------------------------------------------------
// LINKS

/**
 *  Get the text representation of a link.
 *
 * @param link the link to get the text representation.
 * @returns {string} the text representation of the link.
 */
provider.link.getTextValue = function (link) {
    if (provider.link.Provider.hasOwnProperty("getTextValue")) {
        return provider.link.Provider.getTextValue(link);
    } else {
        if (provider.link.DEFAULT_PROVIDER.hasOwnProperty("getTextValue")) {
            return provider.link.DEFAULT_PROVIDER.getTextValue(link);
        } else {
            logger.error("No provider defined for link getTextValue");
        }
    }
};

provider.link.getColor = function (link, element, attribute) {
    if (provider.link.Provider.hasOwnProperty("getColor")) {
        return provider.link.Provider.getColor(link, element, attribute);
    } else {
        if (provider.link.DEFAULT_PROVIDER.hasOwnProperty("getColor")) {
            return provider.link.DEFAULT_PROVIDER.getColor(link, element, attribute);
        } else {
            logger.error("No provider defined for getColor");
        }
    }
};

provider.link.getCSSClass = function (link, element) {
    if (provider.link.Provider.hasOwnProperty("getCSSClass")) {
        return provider.link.Provider.getCSSClass(link, element);
    } else {
        if (provider.link.DEFAULT_PROVIDER.hasOwnProperty("getCSSClass")) {
            return provider.link.DEFAULT_PROVIDER.getCSSClass(link, element);
        } else {
            logger.error("No provider defined for getCSSClass");
        }
    }
};

provider.link.getDistance = function (link) {
    if (provider.link.Provider.hasOwnProperty("getDistance")) {
        return provider.link.Provider.getDistance(link);
    } else {
        if (provider.link.DEFAULT_PROVIDER.hasOwnProperty("getDistance")) {
            return provider.link.DEFAULT_PROVIDER.getDistance(link);
        } else {
            logger.error("No provider defined for getDistance");
        }
    }
};

/**
 *  Get the semantic text representation of a link.
 *
 * @param link the link to get the semantic text representation.
 * @returns {string} the semantic text representation of the link.
 */
provider.link.getSemanticValue = function (link) {
    if (provider.link.Provider.hasOwnProperty("getSemanticValue")) {
        return provider.link.Provider.getSemanticValue(link);
    } else {
        if (provider.link.DEFAULT_PROVIDER.hasOwnProperty("getSemanticValue")) {
            return provider.link.DEFAULT_PROVIDER.getSemanticValue(link);
        } else {
            logger.error("No provider defined for getSemanticValue");
        }
    }
};

provider.colorLuminance = function (hex, lum) {

    // validate hex string
    hex = String(hex).replace(/[^0-9a-f]/gi, '');
    if (hex.length < 6) {
        hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
    }
    lum = lum || 0;

    // convert to decimal and change luminosity
    var rgb = "#", c, i;
    for (i = 0; i < 3; i++) {
        c = parseInt(hex.substr(i * 2, 2), 16);
        c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
        rgb += ("00" + c).substr(c.length);
    }

    return rgb;
};

/**
 * Label provider used by default if none have been defined for a label.
 * This provider can be changed if needed to customize default behavior.
 * If some properties are not found in user customized providers, default values will be extracted from this provider.
 */
provider.link.DEFAULT_PROVIDER = {
    /**
     * Function used to return the text representation of a link.
     *
     * The default behavior is to return the internal relation name as text for relation links.
     * And return the target node text value for links between a node and its expanded values but only if text is not displayed on value node.
     *
     * @param link the link to represent as text.
     * @returns {string} the text representation of the link.
     */
    "getTextValue": function (link) {
        if (link.type === graph$1.link.LinkTypes.VALUE) {
            // Links between node and list of values.

            if (provider.node.isTextDisplayed(link.target)) {
                // Don't display text on link if text is displayed on target node.
                return "";
            } else {
                // No text is displayed on target node then the text is displayed on link.
                return provider.node.getTextValue(link.target);
            }

        } else {
            var targetName = "";
            if (link.type === graph$1.link.LinkTypes.SEGMENT) {
                targetName = " " + provider.node.getTextValue(link.target);
            }
            return link.label + targetName;
        }
    },


    /**
     *
     * @param link
     */
    "getDistance": function (link) {
        if (link.type === graph$1.link.LinkTypes.VALUE) {
            return (13 / 8) * (provider.node.getSize(link.source) + provider.node.getSize(link.target));
        } else {
            return (20 / 8) * (provider.node.getSize(link.source) + provider.node.getSize(link.target));
        }
    },

    /**
     * Return the color to use on links and relation donut segments.
     *
     *
     * Return null or undefined
     * @param link
     * @param element
     * @param attribute
     * @return {*}
     */
    "getColor": function (link, element, attribute) {
        if (link.type === graph$1.link.LinkTypes.VALUE) {
            return "#525863";
        } else {
            var colorId = link.source.label + link.label + link.target.label;

            var color = provider.colorScale(colorId);
            if (attribute === "stroke") {
                return provider.colorLuminance(color, -0.2);
            }
            return color;
        }
    },

    /**
     *
     * @param link
     * @param element
     * @return {string}
     */
    "getCSSClass": function (link, element) {
        var cssClass = "ppt-link__" + element;

        if (link.type === graph$1.link.LinkTypes.VALUE) {
            cssClass = cssClass + "--value";
        } else {
            var labelAsCSSName = "ppt-" + link.label.replace(/[^0-9a-z\-_]/gi, '');
            if (link.type === graph$1.link.LinkTypes.RELATION) {
                cssClass = cssClass + "--relation";

                if (link.target.count === 0) {
                    cssClass = cssClass + "--disabled";
                }

                cssClass = cssClass + " " + labelAsCSSName;
            }
        }

        return cssClass;
    },

    /**
     * Function used to return a descriptive text representation of a link.
     * This representation should be more complete than getLinkTextValue and can contain semantic data.
     * This function is used for example to generate the label in the query viewer.
     *
     * The default behavior is to return the getLinkTextValue.
     *
     * @param link the link to represent as text.
     * @returns {string} the text semantic representation of the link.
     */
    "getSemanticValue": function (link) {
        return provider.link.getTextValue(link);
    }
};

provider.link.Provider = provider.link.DEFAULT_PROVIDER;

//------------------------------------------------
// TAXONOMY

/**
 *  Get the text representation of a taxonomy.
 *
 * @param label the label used for the taxonomy.
 * @returns {string} the text representation of the taxonomy.
 */
provider.taxonomy.getTextValue = function (label) {
    if (provider.taxonomy.Provider.hasOwnProperty("getTextValue")) {
        return provider.taxonomy.Provider.getTextValue(label);
    } else {
        if (provider.taxonomy.DEFAULT_PROVIDER.hasOwnProperty("getTextValue")) {
            return provider.taxonomy.DEFAULT_PROVIDER.getTextValue(label);
        } else {
            logger.error("No provider defined for taxonomy getTextValue");
        }
    }
};

/**
 *
 * @param label
 * @param element
 * @return {*}
 */
provider.taxonomy.getCSSClass = function (label, element) {
    if (provider.taxonomy.Provider.hasOwnProperty("getCSSClass")) {
        return provider.taxonomy.Provider.getCSSClass(label, element);
    } else {
        if (provider.taxonomy.DEFAULT_PROVIDER.hasOwnProperty("getCSSClass")) {
            return provider.taxonomy.DEFAULT_PROVIDER.getCSSClass(label, element);
        } else {
            logger.error("No provider defined for taxonomy getCSSClass");
        }
    }
};

/**
 * Label provider used by default if none have been defined for a label.
 * This provider can be changed if needed to customize default behavior.
 * If some properties are not found in user customized providers, default values will be extracted from this provider.
 */
provider.taxonomy.DEFAULT_PROVIDER = {
    /**
     * Function used to return the text representation of a taxonomy.
     *
     * The default behavior is to return the label without changes.
     *
     * @param label the label used to represent the taxonomy.
     * @returns {string} the text representation of the taxonomy.
     */
    "getTextValue": function (label) {
        return label;
    },

    /**
     *
     * @param label
     * @return {string}
     */
    "getCSSClass": function (label, element) {
        var labelAsCSSName = label.replace(/[^0-9a-z\-_]/gi, '');

        var cssClass = "ppt-taxo__" + element;

        return cssClass + " " + labelAsCSSName;
    }

};

provider.taxonomy.Provider = provider.taxonomy.DEFAULT_PROVIDER;

/**
 * Define the different type of rendering of a node for a given label.
 * TEXT: default rendering type, the node will be displayed with an ellipse and a text in it.
 * IMAGE: the node is displayed as an image using the image tag in the svg graph.
 * In this case an image path is required.
 * SVG: the node is displayed using a list of svg path, each path can contain its own color.
 */
provider.node.DisplayTypes = Object.freeze({TEXT: 0, IMAGE: 1, SVG: 2, SYMBOL: 3});

/**
 * Get the label provider for the given label.
 * If no provider is defined for the label:
 * First search in parent provider.
 * Then if not found will create one from default provider.
 *
 * @param label to retrieve the corresponding label provider.
 * @returns {object} corresponding label provider.
 */
provider.node.getProvider = function (label) {
    if (label === undefined) {
        logger.error("Node label is undefined, no label provider can be found.");
    } else {
        if (provider.node.Provider.hasOwnProperty(label)) {
            return provider.node.Provider[label];
        } else {
            logger.debug("No direct provider found for label " + label);

            // Search in all children list definitions to find the parent provider.
            for (var p in provider.node.Provider) {
                if (provider.node.Provider.hasOwnProperty(p)) {
                    var nProvider = provider.node.Provider[p];
                    if (nProvider.hasOwnProperty("children")) {
                        if (nProvider["children"].indexOf(label) > -1) {
                            logger.debug("No provider is defined for label (" + label + "), parent (" + p + ") will be used");
                            // A provider containing the required label in its children definition has been found it will be cloned.

                            var newProvider = {"parent": p};
                            for (var pr in nProvider) {
                                if (nProvider.hasOwnProperty(pr) && pr !== "children" && pr !== "parent") {
                                    newProvider[pr] = nProvider[pr];
                                }
                            }

                            provider.node.Provider[label] = newProvider;
                            return provider.node.Provider[label];
                        }
                    }
                }
            }

            logger.debug("No label provider defined for label (" + label + ") default one will be created from provider.node.DEFAULT_PROVIDER");

            provider.node.Provider[label] = {};
            // Clone default provider properties in new provider.
            for (var prop in provider.node.DEFAULT_PROVIDER) {
                if (provider.node.DEFAULT_PROVIDER.hasOwnProperty(prop)) {
                    provider.node.Provider[label][prop] = provider.node.DEFAULT_PROVIDER[prop];
                }
            }
            return provider.node.Provider[label];
        }
    }
};

/**
 * Get the property or function defined in node label provider.
 * If the property is not found search is done in parents.
 * If not found in parent, property defined in provider.node.DEFAULT_PROVIDER is returned.
 * If not found in default provider, defaultValue is set and returned.
 *
 * @param label node label to get the property in its provider.
 * @param name name of the property to retrieve.
 * @returns {*} node property defined in its label provider.
 */
provider.node.getProperty = function (label, name) {
    var nProvider = provider.node.getProvider(label);

    if (!nProvider.hasOwnProperty(name)) {
        var providerIterator = nProvider;

        // Check parents
        var isPropertyFound = false;
        while (providerIterator.hasOwnProperty("parent") && !isPropertyFound) {
            providerIterator = provider.node.getProvider(providerIterator.parent);
            if (providerIterator.hasOwnProperty(name)) {

                // Set attribute in child to optimize next call.
                nProvider[name] = providerIterator[name];
                isPropertyFound = true;
            }
        }

        if (!isPropertyFound) {
            logger.debug("No \"" + name + "\" property found for node label provider (" + label + "), default value will be used");
            if (provider.node.DEFAULT_PROVIDER.hasOwnProperty(name)) {
                nProvider[name] = provider.node.DEFAULT_PROVIDER[name];
            } else {
                logger.debug("No default value for \"" + name + "\" property found for label provider (" + label + ")");
            }
        }
    }
    return nProvider[name];
};

/**
 *
 * @param label
 */
provider.node.getIsAutoLoadValue = function (label) {
    return provider.node.getProperty(label, "isAutoLoadValue");
};

/**
 * Return the "isSearchable" property for the node label provider.
 * Is Searchable defines whether the label can be used as graph query builder root.
 * If true the label can be displayed in the taxonomy filter.
 *
 * @param label
 * @returns boolean
 */
provider.node.getIsSearchable = function (label) {
    return provider.node.getProperty(label, "isSearchable");
};

/**
 * Return the "autoExpandRelations" property for the node label provider.
 * Auto expand relations defines whether the label will automatically add its relations when displayed on graph.
 *
 * @param label
 * @returns boolean
 */
provider.node.getIsAutoExpandRelations = function (label) {
    return provider.node.getProperty(label, "autoExpandRelations");
};

provider.node.getSchema = function (label) {
    return provider.node.getProperty(label, "schema");
};

/**
 * Return the list of attributes defined in node label provider.
 * Parents return attributes are also returned.
 *
 * @param label used to retrieve parent attributes.
 * @returns {Array} list of return attributes for a node.
 */
provider.node.getReturnAttributes = function (label) {
    var nProvider = provider.node.getProvider(label);
    var attributes = {}; // Object is used as a Set to merge possible duplicate in parents

    if (nProvider.hasOwnProperty("returnAttributes")) {
        for (var i = 0; i < nProvider.returnAttributes.length; i++) {
            if (nProvider.returnAttributes[i] === query.NEO4J_INTERNAL_ID) {
                attributes[query.NEO4J_INTERNAL_ID.queryInternalName] = true;
            } else {
                attributes[nProvider.returnAttributes[i]] = true;
            }
        }
    }

    // Add parent attributes
    while (nProvider.hasOwnProperty("parent")) {
        nProvider = provider.node.getProvider(nProvider.parent);
        if (nProvider.hasOwnProperty("returnAttributes")) {
            for (var j = 0; j < nProvider.returnAttributes.length; j++) {
                if (nProvider.returnAttributes[j] === query.NEO4J_INTERNAL_ID) {
                    attributes[query.NEO4J_INTERNAL_ID.queryInternalName] = true;
                } else {
                    attributes[nProvider.returnAttributes[j]] = true;
                }
            }
        }
    }

    // Add default provider attributes if any but not internal id as this id is added only if none has been found.
    if (provider.node.DEFAULT_PROVIDER.hasOwnProperty("returnAttributes")) {
        for (var k = 0; k < provider.node.DEFAULT_PROVIDER.returnAttributes.length; k++) {
            if (provider.node.DEFAULT_PROVIDER.returnAttributes[k] !== query.NEO4J_INTERNAL_ID) {
                attributes[provider.node.DEFAULT_PROVIDER.returnAttributes[k]] = true;
            }
        }
    }

    // Add constraint attribute in the list
    var constraintAttribute = provider.node.getConstraintAttribute(label);
    if (constraintAttribute === query.NEO4J_INTERNAL_ID) {
        attributes[query.NEO4J_INTERNAL_ID.queryInternalName] = true;
    } else {
        attributes[constraintAttribute] = true;
    }


    // Add all in array
    var attrList = [];
    for (var attr in attributes) {
        if (attributes.hasOwnProperty(attr)) {
            if (attr === query.NEO4J_INTERNAL_ID.queryInternalName) {
                attrList.push(query.NEO4J_INTERNAL_ID);
            } else {
                attrList.push(attr);
            }
        }
    }

    // If no attributes have been found internal ID is used
    if (attrList.length <= 0) {
        attrList.push(query.NEO4J_INTERNAL_ID);
    }
    return attrList;
};

/**
 * Return the attribute to use as constraint attribute for a node defined in its label provider.
 *
 * @param label
 * @returns {*}
 */
provider.node.getConstraintAttribute = function (label) {
    return provider.node.getProperty(label, "constraintAttribute");
};

provider.node.getDisplayAttribute = function (label) {
    var displayAttribute = provider.node.getProperty(label, "displayAttribute");

    if (displayAttribute === undefined) {
        var returnAttributes = provider.node.getReturnAttributes(label);
        if (returnAttributes.length > 0) {
            displayAttribute = returnAttributes[0];
        } else {
            displayAttribute = provider.node.getConstraintAttribute(label);
        }
    }

    return displayAttribute
};

/**
 * Return a list of predefined constraint defined in the node label configuration.
 *
 * @param label
 * @returns {*}
 */
provider.node.getPredefinedConstraints = function (label) {
    return provider.node.getProperty(label, "getPredefinedConstraints")();
};

provider.node.filterResultQuery = function (label, initialQuery) {
    return provider.node.getProperty(label, "filterResultQuery")(initialQuery);
};

provider.node.getValueOrderByAttribute = function (label) {
    return provider.node.getProperty(label, "valueOrderByAttribute");
};

provider.node.isValueOrderAscending = function (label) {
    return provider.node.getProperty(label, "isValueOrderAscending");
};

provider.node.getResultOrderByAttribute = function (label) {
    return provider.node.getProperty(label, "resultOrderByAttribute");
};

/**
 *
 * @param label
 */
provider.node.isResultOrderAscending = function (label) {
    return provider.node.getProperty(label, "isResultOrderAscending");
};

/**
 * Return the value of the getTextValue function defined in the label provider corresponding to the parameter node.
 * If no "getTextValue" function is defined in the provider, search is done in parents.
 * If none is found in parent default provider method is used.
 *
 * @param node
 * @param parameter
 */
provider.node.getTextValue = function (node, parameter) {
    return provider.node.getProperty(node.label, "getTextValue")(node, parameter);
};


/**
 * Return the value of the getSemanticValue function defined in the label provider corresponding to the parameter node.
 * The semantic value is a more detailed description of the node used for example in the query viewer.
 * If no "getTextValue" function is defined in the provider, search is done in parents.
 * If none is found in parent default provider method is used.
 *
 * @param node
 * @returns {*}
 */
provider.node.getSemanticValue = function (node) {
    return provider.node.getProperty(node.label, "getSemanticValue")(node);
};

/**
 * Return a list of SVG paths objects, each defined by a "d" property containing the path and "f" property for the color.
 *
 * @param node
 * @returns {*}
 */
provider.node.getSVGPaths = function (node) {
    return provider.node.getProperty(node.label, "getSVGPaths")(node);
};

/**
 * Check in label provider if text must be displayed with images nodes.
 * @param node
 * @returns {*}
 */
provider.node.isTextDisplayed = function (node) {
    return provider.node.getProperty(node.label, "getIsTextDisplayed")(node);
};

/**
 *
 * @param node
 */
provider.node.getSize = function (node) {
    return provider.node.getProperty(node.label, "getSize")(node);
};

/**
 * Return the getColor property.
 *
 * @param node
 * @param style
 * @returns {*}
 */
provider.node.getColor = function (node, style) {
    return provider.node.getProperty(node.label, "getColor")(node, style);
};

/**
 *
 * @param node
 * @param element
 */
provider.node.getCSSClass = function (node, element) {
    return provider.node.getProperty(node.label, "getCSSClass")(node, element);
};

/**
 * Return the getIsGroup property.
 *
 * @param node
 * @returns {*}
 */
provider.node.getIsGroup = function (node) {
    return provider.node.getProperty(node.label, "getIsGroup")(node);
};

/**
 * Return the node display type.
 * can be TEXT, IMAGE, SVG or GROUP.
 *
 * @param node
 * @returns {*}
 */
provider.node.getNodeDisplayType = function (node) {
    return provider.node.getProperty(node.label, "getDisplayType")(node);
};

/**
 * Return the file path of the image defined in the provider.
 *
 * @param node the node to get the image path.
 * @returns {string} the path of the node image.
 */
provider.node.getImagePath = function (node) {
    return provider.node.getProperty(node.label, "getImagePath")(node);
};

/**
 * Return the width size of the node image.
 *
 * @param node the node to get the image width.
 * @returns {int} the image width.
 */
provider.node.getImageWidth = function (node) {
    return provider.node.getProperty(node.label, "getImageWidth")(node);
};

/**
 * Return the height size of the node image.
 *
 * @param node the node to get the image height.
 * @returns {int} the image height.
 */
provider.node.getImageHeight = function (node) {
    return provider.node.getProperty(node.label, "getImageHeight")(node);
};

provider.node.filterNodeValueQuery = function (node, initialQuery) {
    return provider.node.getProperty(node.label, "filterNodeValueQuery")(node, initialQuery);
};

provider.node.filterNodeCountQuery = function (node, initialQuery) {
    return provider.node.getProperty(node.label, "filterNodeCountQuery")(node, initialQuery);
};

provider.node.filterNodeRelationQuery = function (node, initialQuery) {
    return provider.node.getProperty(node.label, "filterNodeRelationQuery")(node, initialQuery);
};

provider.node.getGenerateNodeValueConstraints = function (node) {
    return provider.node.getProperty(node.label, "generateNodeValueConstraints");
};

provider.node.getGenerateNegativeNodeValueConstraints = function (node) {
    return provider.node.getProperty(node.label, "generateNegativeNodeValueConstraints");
};

/**
 * Return the displayResults function defined in label parameter's provider.
 *
 * @param label
 * @returns {*}
 */
provider.node.getDisplayResults = function (label) {
    return provider.node.getProperty(label, "displayResults");
};

/**
 * Label provider used by default if none have been defined for a label.
 * This provider can be changed if needed to customize default behavior.
 * If some properties are not found in user customized providers, default
 * values will be extracted from this provider.
 */
provider.node.DEFAULT_PROVIDER = (
    {
        /**********************************************************************
         * Label specific parameters:
         *
         * These attributes are specific to a node label and will be used for
         * every node having this label.
         **********************************************************************/

        /**
         * Defines whether this label can be used as root element of the graph
         * query builder.
         * This property is also used to determine whether the label can be
         * displayed in the taxonomy filter.
         *
         * The default value is true.
         */
        "isSearchable": true,

        /**
         * Defines whether this label will automatically expend its relations
         * when displayed on graph.
         * If set to true, once displayed additional request will be sent on
         * the database to retrieve its relations.
         *
         * The default value is false.
         */
        "autoExpandRelations": false,

        /**
         * Defines whether this label will automatically load its available
         * data displayed on graph.
         * If set to true, once displayed additional request will be sent on
         * the database to retrieve its possible values.
         *
         * The default value is false.
         */
        "isAutoLoadValue": false,

        /**
         * Defines the list of attribute to return for node of this label.
         * All the attributes listed here will be added in generated cypher
         * queries and available in result list and in node provider's
         * functions.
         *
         * The default value contains only the Neo4j internal id.
         * This id is used by default because it is a convenient way to identify
         * a node when nothing is known about its attributes.
         * But you should really consider using your application attributes
         * instead, it is a bad practice to rely on this attribute.
         */
        "returnAttributes": [query.NEO4J_INTERNAL_ID],

        /**
         * Defines the attribute used to order the value displayed on node.
         *
         * Default value is "count" attribute.
         */
        "valueOrderByAttribute": "count",

        /**
         * Defines whether the value query order by is ascending, if false order
         * by will be descending.
         *
         * Default value is false (descending)
         */
        "isValueOrderAscending": false,

        /**
         * Defines the attributes used to order the results.
         * It can be an attribute name or a list of attribute names.
         *
         * Default value is "null" to disable order by.
         */
        "resultOrderByAttribute": null,

        /**
         * Defines whether the result query order by is ascending, if false
         * order by will be descending.
         * It can be a boolean value or a list of boolean to match the resultOrderByAttribute.
         * If size of isResultOrderAscending < size of resultOrderByAttribute last value is used.
         *
         * Default value is true (ascending)
         */
        "isResultOrderAscending": true,

        /**
         * Defines the attribute of the node to use in query constraint for
         * nodes of this label.
         * This attribute is used in the generated cypher query to build the
         * constraints with selected values.
         *
         * The default value is the Neo4j internal id.
         * This id is used by default because it is a convenient way to
         * identify a node when nothing is known about its attributes.
         * But you should really consider using your application attributes
         * instead, it is a bad practice to rely on this attribute.
         */
        "constraintAttribute": query.NEO4J_INTERNAL_ID,

        /**
         * Defines the attribute of the node to use by default to display the node.
         * This attribute must be present in returnAttributes list.
         *
         * The default value is undefined.
         * If undefined the first attribute of the returnAttributes will be used or
         * constraintAttribute if the list is empty.
         */
        "displayAttribute": undefined,

        /**
         * Return the list of predefined constraints to add for the given label.
         * These constraints will be added in every generated Cypher query.
         *
         * For example if the returned list contain ["$identifier.born > 1976"]
         * for "Person" nodes everywhere in popoto.js the generated Cypher
         * query will add the constraint "WHERE person.born > 1976"
         *
         * @returns {Array}
         */
        "getPredefinedConstraints": function () {
            return [];
        },

        /**
         * Filters the query generated to retrieve the queries.
         *
         * @param initialQuery contains the query as an object structure.
         * @returns {*}
         */
        "filterResultQuery": function (initialQuery) {
            return initialQuery;
        },

        /**********************************************************************
         * Node specific parameters:
         *
         * These attributes are specific to nodes (in graph or query viewer)
         * for a given label.
         * But they can be customized for nodes of the same label.
         * The parameters are defined by a function that will be called with
         * the node as parameter.
         * In this function the node internal attributes can be used to
         * customize the value to return.
         **********************************************************************/

        /**
         * Function returning the display type of a node.
         * This type defines how the node will be drawn in the graph.
         *
         * The result must be one of the following values:
         *
         * provider.node.DisplayTypes.IMAGE
         *  In this case the node will be drawn as an image and "getImagePath"
         *  function is required to return the node image path.
         *
         * provider.node.DisplayTypes.SVG
         *  In this case the node will be drawn as SVG paths and "getSVGPaths"
         *
         * provider.node.DisplayTypes.TEXT
         *  In this case the node will be drawn as a simple circle.
         *
         * Default value is TEXT.
         *
         * @param node the node to extract its type.
         * @returns {number} one value from provider.node.DisplayTypes
         */
        "getDisplayType": function (node) {
            return provider.node.DisplayTypes.TEXT;
        },

        /**
         * Function defining the size of the node in graph.
         *
         * The size define the radius of the circle defining the node.
         * other elements (menu, counts...) will scale on this size.
         *
         * Default value is 50.
         *
         * @param node
         */
        "getSize": function (node) {
            return 50;
        },

        /**
         * Return a color for the node.
         *
         * @param node
         * @returns {*}
         */
        "getColor": function (node) {
            if (node.type === graph$1.node.NodeTypes.VALUE) {
                return provider.node.getColor(node.parent);
            } else {
                var parentLabel = "";
                if (node.hasOwnProperty("parent")) {
                    parentLabel = node.parent.label;
                }

                var incomingRelation = node.parentRel || "";

                var colorId = parentLabel + incomingRelation + node.label;
                return provider.colorScale(colorId);
            }
        },

        /**
         * Generate a CSS class for the node depending on its type.
         *
         * @param node
         * @param element
         * @return {string}
         */
        "getCSSClass": function (node, element) {
            var labelAsCSSName = node.label.replace(/[^0-9a-z\-_]/gi, '');

            var cssClass = "ppt-node__" + element;

            if (node.type === graph$1.node.NodeTypes.ROOT) {
                cssClass = cssClass + "--root";
            }
            if (node.type === graph$1.node.NodeTypes.CHOOSE) {
                cssClass = cssClass + "--choose";
            }
            if (node.type === graph$1.node.NodeTypes.GROUP) {
                cssClass = cssClass + "--group";
            }
            if (node.type === graph$1.node.NodeTypes.VALUE) {
                cssClass = cssClass + "--value";
            }
            if (node.value !== undefined && node.value.length > 0) {
                cssClass = cssClass + "--value-selected";
            }
            if (node.count === 0) {
                cssClass = cssClass + "--disabled";
            }

            return cssClass + " " + labelAsCSSName;
        },

        /**
         * Function defining whether the node is a group node.
         * In this case no count are displayed and no value can be selected on
         * the node.
         *
         * Default value is false.
         */
        "getIsGroup": function (node) {
            return false;
        },

        /**
         * Function defining whether the node text representation must be
         * displayed on graph.
         * If true the value returned for getTextValue on node will be displayed
         * on graph.
         *
         * This text will be added in addition to the getDisplayType
         * representation.
         * It can be displayed on all type of node display, images, SVG or text.
         *
         * Default value is true
         *
         * @param node the node to display on graph.
         * @returns {boolean} true if text must be displayed on graph for the
         * node.
         */
        "getIsTextDisplayed": function (node) {
            return true;
        },

        /**
         * Function used to return the text representation of a node.
         *
         * The default behavior is to return the label of the node
         * or the value of constraint attribute of the node if it contains
         * value.
         *
         * The returned value is truncated using
         * graph.node.NODE_MAX_CHARS property.
         *
         * @param node the node to represent as text.
         * @param maxLength used to truncate the text.
         * @returns {string} the text representation of the node.
         */
        "getTextValue": function (node, maxLength) {
            var text = "";
            var displayAttr = provider.node.getDisplayAttribute(node.label);
            if (node.type === graph$1.node.NodeTypes.VALUE) {
                if (displayAttr === query.NEO4J_INTERNAL_ID) {
                    text = "" + node.internalID;
                } else {
                    text = "" + node.attributes[displayAttr];
                }
            } else {
                if (node.value !== undefined && node.value.length > 0) {
                    if (displayAttr === query.NEO4J_INTERNAL_ID) {
                        var separator = "";
                        node.value.forEach(function (value) {
                            text += separator + value.internalID;
                            separator = " or ";
                        });
                    } else {
                        var separator = "";
                        node.value.forEach(function (value) {
                            text += separator + value.attributes[displayAttr];
                            separator = " or ";
                        });
                    }
                } else {
                    text = node.label;
                }
            }

            return text;
        },

        /**
         * Function used to return a descriptive text representation of a link.
         * This representation should be more complete than getTextValue and can
         * contain semantic data.
         * This function is used for example to generate the label in the query
         * viewer.
         *
         * The default behavior is to return the getTextValue not truncated.
         *
         * @param node the node to represent as text.
         * @returns {string} the text semantic representation of the node.
         */
        "getSemanticValue": function (node) {
            var text = "";
            var displayAttr = provider.node.getDisplayAttribute(node.label);
            if (node.type === graph$1.node.NodeTypes.VALUE) {
                if (displayAttr === query.NEO4J_INTERNAL_ID) {
                    text = "" + node.internalID;
                } else {
                    text = "" + node.attributes[displayAttr];
                }
            } else {
                if (node.value !== undefined && node.value.length > 0) {
                    if (displayAttr === query.NEO4J_INTERNAL_ID) {
                        var separator = "";
                        node.value.forEach(function (value) {
                            text += separator + value.internalID;
                            separator = " or ";
                        });
                    } else {
                        var separator = "";
                        node.value.forEach(function (value) {
                            text += separator + value.attributes[displayAttr];
                            separator = " or ";
                        });
                    }
                } else {
                    text = node.label;
                }
            }
            return text;
        },

        /**
         * Function returning the image file path to use for a node.
         * This function is only used for provider.node.DisplayTypes.IMAGE
         * type nodes.
         *
         * @param node
         * @returns {string}
         */
        "getImagePath": function (node) {
            // if (node.type === graph.node.NodeTypes.VALUE) {
            //     var constraintAttribute = provider.node.getConstraintAttribute(node.label);
            //     return "image/node/value/" + node.label.toLowerCase() + "/" + node.attributes[constraintAttribute] + ".svg";
            // } else {
            return "image/node/" + node.label.toLowerCase() + "/" + node.label.toLowerCase() + ".svg";
            // }
        },

        /**
         * Function returning a array of path objects to display in the node.
         *
         * @param node
         * @returns {*[]}
         */
        "getSVGPaths": function (node) {
            var size = provider.node.getSize(node);
            return [
                {
                    "d": "M 0, 0 m -" + size + ", 0 a " + size + "," + size + " 0 1,0 " + 2 * size + ",0 a " + size + "," + size + " 0 1,0 -" + 2 * size + ",0",
                    "fill": "transparent",
                    "stroke": provider.node.getColor(node),
                    "stroke-width": "2px"
                }
            ];
        },

        /**
         * Function returning the image width of the node.
         * This function is only used for provider.node.DisplayTypes.IMAGE
         * type nodes.
         *
         * @param node
         * @returns {number}
         */
        "getImageWidth": function (node) {
            return provider.node.getSize(node) * 2;
        },

        /**
         * Function returning the image height of the node.
         * This function is only used for provider.node.DisplayTypes.IMAGE
         * type nodes.
         *
         * @param node
         * @returns {number}
         */
        "getImageHeight": function (node) {
            return provider.node.getSize(node) * 2;
        },

        /**
         * Filters the query generated to retrieve the values on a node.
         *
         * @param node
         * @param initialQuery contains the query as an object structure.
         * @returns {*}
         */
        "filterNodeValueQuery": function (node, initialQuery) {
            return initialQuery;
        },
        /**
         * Filters the query generated to retrieve the values on a node.
         *
         * @param node
         * @param initialQuery contains the query as an object structure.
         * @returns {*}
         */
        "filterNodeCountQuery": function (node, initialQuery) {
            return initialQuery;
        },
        /**
         * Filters the query used to retrieve the values on a node.
         *
         * @param node
         * @param initialQuery contains the query as an object structure.
         * @returns {*}
         */
        "filterNodeRelationQuery": function (node, initialQuery) {
            return initialQuery;
        },

        /**
         * Customize, in query, the generated constraint for the node.
         *
         * If undefined use default constraint generation.
         */
        "generateNodeValueConstraints": undefined,

        /**
         * Customize, in query, the generated negative constraint for the node.
         *
         * If undefined use default negative constraint generation.
         */
        "generateNegativeNodeValueConstraints": undefined,

        /**********************************************************************
         * Results specific parameters:
         *
         * These attributes are specific to result display.
         **********************************************************************/

        /**
         * Generate the result entry content using d3.js mechanisms.
         *
         * The parameter of the function is the &lt;p&gt; selected with d3.js
         *
         * The default behavior is to generate a &lt;table&gt; containing all
         * the return attributes in a &lt;th&gt; and their value in a &lt;td&gt;.
         *
         * @param pElmt the &lt;p&gt; element generated in the result list.
         */
        "displayResults": function (pElmt) {
            var result = pElmt.data()[0];
            var returnAttributes = provider.node.getReturnAttributes(result.label);

            returnAttributes.forEach(function (attribute) {
                var div = pElmt.append("div").attr("class", "ppt-result-attribute-div");
                var attributeName = attribute;

                if (query.NEO4J_INTERNAL_ID === attribute) {
                    attributeName = query.NEO4J_INTERNAL_ID.queryInternalName;
                }

                var span = div.append("span");
                span.text(function () {
                    if (attribute === query.NEO4J_INTERNAL_ID) {
                        return "internal ID:"
                    } else {
                        return attribute + ":";
                    }
                });
                if (result.attributes[attributeName] !== undefined) {
                    div.append("span").text(function (result) {
                        return result.attributes[attributeName];
                    });
                }
            });
        }
    });

var provider$1 = provider;

var queryviewer = {};

queryviewer.containerId = "popoto-query";
queryviewer.QUERY_STARTER = "I'm looking for";
queryviewer.CHOOSE_LABEL = "choose";

/**
 * Create the query viewer area.
 *
 */
queryviewer.createQueryArea = function () {
    var id = "#" + queryviewer.containerId;

    queryviewer.queryConstraintSpanElements = d3__namespace.select(id).append("p").attr("class", "ppt-query-constraint-elements").selectAll(".queryConstraintSpan");
    queryviewer.querySpanElements = d3__namespace.select(id).append("p").attr("class", "ppt-query-elements").selectAll(".querySpan");
};

/**
 * Update all the elements displayed on the query viewer based on current graph.
 */
queryviewer.updateQuery = function () {

    // Remove all query span elements
    queryviewer.queryConstraintSpanElements = queryviewer.queryConstraintSpanElements.data([]);
    queryviewer.querySpanElements = queryviewer.querySpanElements.data([]);

    queryviewer.queryConstraintSpanElements.exit().remove();
    queryviewer.querySpanElements.exit().remove();

    // Update data
    queryviewer.queryConstraintSpanElements = queryviewer.queryConstraintSpanElements.data(queryviewer.generateConstraintData(dataModel.links, dataModel.nodes));
    queryviewer.querySpanElements = queryviewer.querySpanElements.data(queryviewer.generateData(dataModel.links, dataModel.nodes));

    // Remove old span (not needed as all have been cleaned before)
    // queryviewer.querySpanElements.exit().remove();

    // Add new span
    queryviewer.queryConstraintSpanElements = queryviewer.queryConstraintSpanElements.enter().append("span")
        .on("contextmenu", queryviewer.rightClickSpan)
        .on("click", queryviewer.clickSpan)
        .on("mouseover", queryviewer.mouseOverSpan)
        .on("mouseout", queryviewer.mouseOutSpan)
        .merge(queryviewer.queryConstraintSpanElements);

    queryviewer.querySpanElements = queryviewer.querySpanElements.enter().append("span")
        .on("contextmenu", queryviewer.rightClickSpan)
        .on("click", queryviewer.clickSpan)
        .on("mouseover", queryviewer.mouseOverSpan)
        .on("mouseout", queryviewer.mouseOutSpan)
        .merge(queryviewer.querySpanElements);

    // Update all span
    queryviewer.queryConstraintSpanElements
        .attr("id", function (d) {
            return d.id
        })
        .attr("class", function (d) {
            if (d.isLink) {
                return "ppt-span-link";
            } else {
                if (d.type === graph$1.node.NodeTypes.ROOT) {
                    return "ppt-span-root";
                } else if (d.type === graph$1.node.NodeTypes.CHOOSE) {
                    if (d.ref.value !== undefined && d.ref.value.length > 0) {
                        return "ppt-span-value";
                    } else {
                        return "ppt-span-choose";
                    }
                } else if (d.type === graph$1.node.NodeTypes.VALUE) {
                    return "ppt-span-value";
                } else if (d.type === graph$1.node.NodeTypes.GROUP) {
                    return "ppt-span-group";
                } else {
                    return "ppt-span";
                }
            }
        })
        .text(function (d) {
            return d.term + " ";
        });

    queryviewer.querySpanElements
        .attr("id", function (d) {
            return d.id
        })
        .attr("class", function (d) {
            if (d.isLink) {
                return "ppt-span-link";
            } else {
                if (d.type === graph$1.node.NodeTypes.ROOT) {
                    return "ppt-span-root";
                } else if (d.type === graph$1.node.NodeTypes.CHOOSE) {
                    if (d.ref.value !== undefined && d.ref.value.length > 0) {
                        return "ppt-span-value";
                    } else {
                        return "ppt-span-choose";
                    }
                } else if (d.type === graph$1.node.NodeTypes.VALUE) {
                    return "ppt-span-value";
                } else if (d.type === graph$1.node.NodeTypes.GROUP) {
                    return "ppt-span-group";
                } else {
                    return "ppt-span";
                }
            }
        })
        .text(function (d) {
            return d.term + " ";
        });
};

queryviewer.generateConstraintData = function (links, nodes) {
    var elmts = [], id = 0;

    // Add query starter
    elmts.push(
        {id: id++, term: queryviewer.QUERY_STARTER}
    );

    // Add the root node as query term
    if (nodes.length > 0) {
        elmts.push(
            {id: id++, type: nodes[0].type, term: provider$1.node.getSemanticValue(nodes[0]), ref: nodes[0]}
        );
    }

    // Add a span for each link and its target node
    links.forEach(function (l) {

        var sourceNode = l.source;
        var targetNode = l.target;
        if (l.type === graph$1.link.LinkTypes.RELATION && targetNode.type !== graph$1.node.NodeTypes.GROUP && targetNode.value !== undefined && targetNode.value.length > 0) {
            if (sourceNode.type === graph$1.node.NodeTypes.GROUP) {
                elmts.push(
                    {
                        id: id++,
                        type: sourceNode.type,
                        term: provider$1.node.getSemanticValue(sourceNode),
                        ref: sourceNode
                    }
                );
            }

            elmts.push({id: id++, isLink: true, term: provider$1.link.getSemanticValue(l), ref: l});

            if (targetNode.type !== graph$1.node.NodeTypes.GROUP) {
                if (targetNode.value !== undefined && targetNode.value.length > 0) {
                    elmts.push(
                        {
                            id: id++,
                            type: targetNode.type,
                            term: provider$1.node.getSemanticValue(targetNode),
                            ref: targetNode
                        }
                    );
                } else {
                    elmts.push(
                        {
                            id: id++,
                            type: targetNode.type,
                            term: "<" + queryviewer.CHOOSE_LABEL + " " + provider$1.node.getSemanticValue(targetNode) + ">",
                            ref: targetNode
                        }
                    );
                }
            }
        }
    });

    return elmts;
};

// TODO add option nodes in generated query when no value is available
queryviewer.generateData = function (links, nodes) {
    var elmts = [], options = [], id = 0;

    // Add a span for each link and its target node
    links.forEach(function (l) {

        var sourceNode = l.source;
        var targetNode = l.target;

        if (targetNode.type === graph$1.node.NodeTypes.GROUP) {
            options.push(
                {
                    id: id++,
                    type: targetNode.type,
                    term: provider$1.node.getSemanticValue(targetNode),
                    ref: targetNode
                }
            );
        }

        if (l.type === graph$1.link.LinkTypes.RELATION && targetNode.type !== graph$1.node.NodeTypes.GROUP && (targetNode.value === undefined || targetNode.value.length === 0)) {
            if (sourceNode.type === graph$1.node.NodeTypes.GROUP) {
                elmts.push(
                    {
                        id: id++,
                        type: sourceNode.type,
                        term: provider$1.node.getSemanticValue(sourceNode),
                        ref: sourceNode
                    }
                );
            }

            elmts.push({id: id++, isLink: true, term: provider$1.link.getSemanticValue(l), ref: l});

            if (targetNode.type !== graph$1.node.NodeTypes.GROUP) {
                elmts.push(
                    {
                        id: id++,
                        type: targetNode.type,
                        term: "<" + queryviewer.CHOOSE_LABEL + " " + provider$1.node.getSemanticValue(targetNode) + ">",
                        ref: targetNode
                    }
                );
            }
        }
    });

    return elmts.concat(options);
};

/**
 *
 */
queryviewer.mouseOverSpan = function () {
    d3__namespace.select(this).classed("hover", function (d) {
        return d.ref;
    });

    var hoveredSpan = d3__namespace.select(this).data()[0];

    if (hoveredSpan.ref) {
        var linkElmt = graph$1.svg.selectAll("#" + graph$1.link.gID + " > g").filter(function (d) {
            return d === hoveredSpan.ref;
        });
        linkElmt.select("path").classed("ppt-link-hover", true);
        linkElmt.select("text").classed("ppt-link-hover", true);

        var nodeElmt = graph$1.svg.selectAll("#" + graph$1.node.gID + " > g").filter(function (d) {
            return d === hoveredSpan.ref;
        });

        nodeElmt.select(".ppt-g-node-background").selectAll("circle").transition().style("fill-opacity", 0.5);

        if (cypherviewer.isActive) {
            cypherviewer.querySpanElements.filter(function (d) {
                return d.node === hoveredSpan.ref || d.link === hoveredSpan.ref;
            }).classed("hover", true);
        }
    }
};

queryviewer.rightClickSpan = function () {
    var clickedSpan = d3__namespace.select(this).data()[0];

    if (!clickedSpan.isLink && clickedSpan.ref) {
        var nodeElmt = graph$1.svg.selectAll("#" + graph$1.node.gID + " > g").filter(function (d) {
            return d === clickedSpan.ref;
        });

        nodeElmt.on("contextmenu").call(nodeElmt.node(), clickedSpan.ref);
    }
};

queryviewer.clickSpan = function () {
    var clickedSpan = d3__namespace.select(this).data()[0];

    if (!clickedSpan.isLink && clickedSpan.ref) {
        var nodeElmt = graph$1.svg.selectAll("#" + graph$1.node.gID + " > g").filter(function (d) {
            return d === clickedSpan.ref;
        });

        nodeElmt.on("click").call(nodeElmt.node(), clickedSpan.ref);
    }
};

/**
 *
 */
queryviewer.mouseOutSpan = function () {
    d3__namespace.select(this).classed("hover", false);

    var hoveredSpan = d3__namespace.select(this).data()[0];

    if (hoveredSpan.ref) {
        var linkElmt = graph$1.svg.selectAll("#" + graph$1.link.gID + " > g").filter(function (d) {
            return d === hoveredSpan.ref;
        });
        linkElmt.select("path").classed("ppt-link-hover", false);
        linkElmt.select("text").classed("ppt-link-hover", false);

        var nodeElmt = graph$1.svg.selectAll("#" + graph$1.node.gID + " > g").filter(function (d) {
            return d === hoveredSpan.ref;
        });
        nodeElmt.select(".ppt-g-node-background").selectAll("circle").transition().style("fill-opacity", 0);

        if (cypherviewer.isActive) {
            cypherviewer.querySpanElements.filter(function (d) {
                return d.node === hoveredSpan.ref || d.link === hoveredSpan.ref;
            }).classed("hover", false);
        }
    }
};

var cypherviewer = {};
cypherviewer.containerId = "popoto-cypher";
cypherviewer.MATCH = "MATCH";
cypherviewer.RETURN = "RETURN";
cypherviewer.WHERE = "WHERE";
cypherviewer.QueryElementTypes = Object.freeze({
    KEYWORD: 0,
    NODE: 1,
    SEPARATOR: 2,
    SOURCE: 3,
    LINK: 4,
    TARGET: 5,
    RETURN: 6,
    WHERE: 7
});

/**
 * Create the Cypher viewer area.
 *
 */
cypherviewer.createQueryArea = function () {
    var id = "#" + cypherviewer.containerId;

    cypherviewer.querySpanElements = d3__namespace.select(id).append("p").attr("class", "ppt-query-constraint-elements").selectAll(".queryConstraintSpan");
};

/**
 * Update all the elements displayed on the cypher viewer based on current graph.
 */
cypherviewer.updateQuery = function () {

    // Remove all query span elements
    cypherviewer.querySpanElements = cypherviewer.querySpanElements.data([]);

    cypherviewer.querySpanElements.exit().remove();

    // Update data
    cypherviewer.querySpanElements = cypherviewer.querySpanElements.data(cypherviewer.generateData(dataModel.links, dataModel.nodes));

    // Remove old span (not needed as all have been cleaned before)
    // queryviewer.querySpanElements.exit().remove();

    // Add new span
    cypherviewer.querySpanElements = cypherviewer.querySpanElements.enter().append("span")
        .attr("id", function (d) {
            return "cypher-" + d.id;
        })
        .on("mouseover", cypherviewer.mouseOverSpan)
        .on("mouseout", cypherviewer.mouseOutSpan)
        .on("contextmenu", cypherviewer.rightClickSpan)
        .on("click", cypherviewer.clickSpan)
        .merge(cypherviewer.querySpanElements);

    // Update all spans:
    cypherviewer.querySpanElements.filter(function (d) {
        return d.type === cypherviewer.QueryElementTypes.KEYWORD;
    })
        .attr("class", "ppt-span")
        .text(function (d) {
            return " " + d.value + " ";
        });

    cypherviewer.querySpanElements.filter(function (d) {
        return d.type === cypherviewer.QueryElementTypes.SEPARATOR;
    })
        .attr("class", "ppt-span")
        .text(function (d) {
            return d.value + " ";
        });

    cypherviewer.querySpanElements.filter(function (d) {
        return d.type === cypherviewer.QueryElementTypes.NODE;
    })
        .attr("class", function (d) {
            if (d.node.value !== undefined && d.node.value.length > 0) {
                return "ppt-span-root-value";
            } else {
                return "ppt-span-root";
            }
        })
        .text(function (d) {
            return "(" + d.node.internalLabel + ":`" + d.node.label + "`)";
        });

    cypherviewer.querySpanElements.filter(function (d) {
        return d.type === cypherviewer.QueryElementTypes.SOURCE;
    })
        .attr("class", function (d) {
            if (d.node === dataModel.getRootNode()) {
                if (d.node.value !== undefined && d.node.value.length > 0) {
                    return "ppt-span-root-value";
                } else {
                    return "ppt-span-root";
                }
            } else {
                if (d.node.value !== undefined && d.node.value.length > 0) {
                    return "ppt-span-value";
                } else {
                    return "ppt-span-choose";
                }
            }
        })
        .text(function (d) {
            var sourceNode = d.node;
            return "(" + sourceNode.internalLabel + ":`" + sourceNode.label + "`)";
        });

    cypherviewer.querySpanElements.filter(function (d) {
        return d.type === cypherviewer.QueryElementTypes.LINK;
    })
        .attr("class", "ppt-span-link")
        .text(function (d) {
            if (d.link.target.isParentRelReverse === true) {
                return "<-[:`" + d.link.label + "`]-";
            } else {
                return "-[:`" + d.link.label + "`]-" + (query.USE_RELATION_DIRECTION ? ">" : "");
            }
        });

    cypherviewer.querySpanElements.filter(function (d) {
        return d.type === cypherviewer.QueryElementTypes.TARGET;
    })
        .attr("class", function (d) {
            if (d.node.value !== undefined && d.node.value.length > 0) {
                return "ppt-span-value";
            } else {
                return "ppt-span-choose";
            }
        })
        .text(function (d) {
            return "(" + d.node.internalLabel + ":`" + d.node.label + "`)";
        });

    cypherviewer.querySpanElements.filter(function (d) {
        return d.type === cypherviewer.QueryElementTypes.WHERE;
    })
        .attr("class", function (d) {
            if (d.node === dataModel.getRootNode()) {
                return "ppt-span-root-value";
            } else {
                return "ppt-span-value";
            }
        })
        .text(function (d) {
            var node = d.node;
            if (node.isNegative === true) {
                if (!node.hasOwnProperty("value") || node.value.length <= 0) {
                    return "(NOT (" + d.link.source.internalLabel + ":`" + d.link.source.label + "`)-[:`" + d.link.label + "`]->(:`" + d.link.target.label + "`))";
                } else {
                    var clauses = [];
                    var constAttr = provider$1.node.getConstraintAttribute(node.label);
                    node.value.forEach(function (value) {
                        clauses.push(
                            "(NOT (" + d.link.source.internalLabel + ":`" + d.link.source.label + "`)-[:`" + d.link.label + "`]->(:`" + d.link.target.label + "`{" + constAttr + ":" + value.attributes[constAttr] + "}))"
                        );
                    });

                    return clauses.join(" AND ");
                }
            } else {
                var constraintAttr = provider$1.node.getConstraintAttribute(node.label);

                var text = "";
                var separator = "";

                if (constraintAttr === query.NEO4J_INTERNAL_ID) {
                    text += node.internalLabel + ".id";
                } else {
                    text += node.internalLabel + "." + constraintAttr;
                }

                if (node.hasOwnProperty("value") && node.value.length > 1) {
                    text += " IN [";
                } else {
                    text += " = ";
                }

                node.value.forEach(function (value) {
                    text += separator;
                    separator = ", ";
                    if (constraintAttr === query.NEO4J_INTERNAL_ID) {
                        text += value.internalID;
                    } else {
                        var constraintValue = value.attributes[constraintAttr];
                        if (typeof constraintValue === "boolean" || typeof constraintValue === "number") {
                            text += constraintValue;
                        } else {
                            text += "\"" + constraintValue + "\"";
                        }
                    }
                });

                if (node.value.length > 1) {
                    text += "]";
                }

                return "(" + text + ")";
            }
        });

    cypherviewer.querySpanElements.filter(function (d) {
        return d.type === cypherviewer.QueryElementTypes.RETURN;
    })
        .attr("class", function (d) {
            if (d.node.value !== undefined && d.node.value.length > 0) {
                return "ppt-span-root-value";
            } else {
                return "ppt-span-root";
            }
        })
        .text(function (d) {
            return d.node.internalLabel;
        });

};

/**
 * Generate the data from graph to use in cypher query viewer.
 * The returned data is a list of elements representing the current query.
 * Example:
 *
 * MATCH
 * (person:`Person`),
 * (person:`Person`)-[:`FOLLOWS`]->(person1:`Person`{`name`:\"Jessica Thompson\"}),
 * (person1:`Person`)-[:`REVIEWED`]->(movie5:`Movie`{`title`:\"The Replacements\"})
 * RETURN person
 *
 * @param links
 * @returns {Array}
 */
cypherviewer.generateData = function (links) {
    var elmts = [], id = 0;
    var rootNode = dataModel.getRootNode();
    var relevantLinks = query.getRelevantLinks(rootNode, rootNode, links);
    var negativeLinks = relevantLinks.filter(function (rl) {
        return rl.target.isNegative === true && (!rl.target.hasOwnProperty("value") || rl.target.value.length <= 0)
    });

    elmts.push(
        {
            id: id++,
            type: cypherviewer.QueryElementTypes.KEYWORD,
            value: cypherviewer.MATCH
        }
    );

    if (rootNode) {
        elmts.push(
            {
                id: id++,
                type: cypherviewer.QueryElementTypes.NODE,
                node: rootNode
            }
        );
    }

    if (relevantLinks.length > 0 && relevantLinks.length > negativeLinks.length) {
        elmts.push(
            {
                id: id++,
                type: cypherviewer.QueryElementTypes.SEPARATOR,
                value: ","
            }
        );
    }

    for (var i = 0; i < relevantLinks.length; i++) {
        var relevantLink = relevantLinks[i];

        if (relevantLink.target.isNegative === true && (!relevantLink.target.hasOwnProperty("value") || relevantLink.target.value.length <= 0)) ; else {
            elmts.push(
                {
                    id: id++,
                    type: cypherviewer.QueryElementTypes.SOURCE,
                    node: relevantLink.source
                }
            );

            elmts.push(
                {
                    id: id++,
                    type: cypherviewer.QueryElementTypes.LINK,
                    link: relevantLink
                }
            );

            elmts.push(
                {
                    id: id++,
                    type: cypherviewer.QueryElementTypes.TARGET,
                    node: relevantLink.target
                }
            );

            // Add separator except for last element
            if (i < (relevantLinks.length - 1)) {
                elmts.push(
                    {
                        id: id++,
                        type: cypherviewer.QueryElementTypes.SEPARATOR,
                        value: ","
                    }
                );
            }
        }
    }

    if ((rootNode && rootNode.value !== undefined && rootNode.value.length > 0) || (relevantLinks.length > 0)) {
        elmts.push(
            {
                id: id++,
                type: cypherviewer.QueryElementTypes.KEYWORD,
                value: cypherviewer.WHERE
            }
        );
    }

    if (rootNode && rootNode.value !== undefined && rootNode.value.length > 0) {
        elmts.push(
            {
                id: id++,
                type: cypherviewer.QueryElementTypes.WHERE,
                node: rootNode
            }
        );

        if (relevantLinks.length > 0) {
            elmts.push(
                {
                    id: id++,
                    type: cypherviewer.QueryElementTypes.SEPARATOR,
                    value: " AND "
                }
            );
        }
    }

    var needSeparator = false;
    for (var i = 0; i < relevantLinks.length; i++) {
        var relevantLink = relevantLinks[i];
        if (relevantLink.target.isNegative === true) {
            if (needSeparator) {
                elmts.push(
                    {
                        id: id++,
                        type: cypherviewer.QueryElementTypes.SEPARATOR,
                        value: " AND "
                    }
                );
            }
            elmts.push(
                {
                    id: id++,
                    type: cypherviewer.QueryElementTypes.WHERE,
                    node: relevantLink.target,
                    link: relevantLink
                }
            );
            needSeparator = true;
        } else {
            if (relevantLink.target.value !== undefined && relevantLink.target.value.length > 0) {

                if (needSeparator) {
                    elmts.push(
                        {
                            id: id++,
                            type: cypherviewer.QueryElementTypes.SEPARATOR,
                            value: " AND "
                        }
                    );
                }

                elmts.push(
                    {
                        id: id++,
                        type: cypherviewer.QueryElementTypes.WHERE,
                        node: relevantLink.target
                    }
                );

                needSeparator = true;
            }
        }
    }

    elmts.push(
        {
            id: id++,
            type: cypherviewer.QueryElementTypes.KEYWORD,
            value: cypherviewer.RETURN
        }
    );

    if (rootNode) {
        elmts.push(
            {
                id: id++,
                type: cypherviewer.QueryElementTypes.RETURN,
                node: rootNode
            }
        );
    }

    return elmts;
};

/**
 *
 */
cypherviewer.mouseOverSpan = function () {
    var hoveredSpan = d3__namespace.select(this).data()[0];
    if (hoveredSpan.node) {
        // Hover all spans with same node data
        cypherviewer.querySpanElements.filter(function (d) {
            return d.node === hoveredSpan.node;
        }).classed("hover", true);

        // Highlight node in graph
        var nodeElmt = graph$1.svg.selectAll("#" + graph$1.node.gID + " > g").filter(function (d) {
            return d === hoveredSpan.node;
        });
        nodeElmt.select(".ppt-g-node-background").selectAll("circle").transition().style("fill-opacity", 0.5);

        // Highlight query viewer
        if (queryviewer.isActive) {
            queryviewer.queryConstraintSpanElements.filter(function (d) {
                return d.ref === hoveredSpan.node;
            }).classed("hover", true);

            queryviewer.querySpanElements.filter(function (d) {
                return d.ref === hoveredSpan.node;
            }).classed("hover", true);
        }
    } else if (hoveredSpan.link) {
        d3__namespace.select(this).classed("hover", true);

        // Highlight link in graph
        var linkElmt = graph$1.svg.selectAll("#" + graph$1.link.gID + " > g").filter(function (d) {
            return d === hoveredSpan.link;
        });
        linkElmt.select("path").classed("ppt-link-hover", true);
        linkElmt.select("text").classed("ppt-link-hover", true);
    }
};

cypherviewer.mouseOutSpan = function () {
    var hoveredSpan = d3__namespace.select(this).data()[0];
    if (hoveredSpan.node) {
        // Remove hover on all spans with same node data
        cypherviewer.querySpanElements.filter(function (d) {
            return d.node === hoveredSpan.node;
        }).classed("hover", false);

        // Remove highlight on node in graph
        var nodeElmt = graph$1.svg.selectAll("#" + graph$1.node.gID + " > g").filter(function (d) {
            return d === hoveredSpan.node;
        });
        nodeElmt.select(".ppt-g-node-background").selectAll("circle").transition().style("fill-opacity", 0);

        if (queryviewer.isActive) {
            queryviewer.queryConstraintSpanElements.filter(function (d) {
                return d.ref === hoveredSpan.node;
            }).classed("hover", false);

            queryviewer.querySpanElements.filter(function (d) {
                return d.ref === hoveredSpan.node;
            }).classed("hover", false);
        }
    } else if (hoveredSpan.link) {
        d3__namespace.select(this).classed("hover", false);

        // Remove highlight on link in graph
        var linkElmt = graph$1.svg.selectAll("#" + graph$1.link.gID + " > g").filter(function (d) {
            return d === hoveredSpan.link;
        });
        linkElmt.select("path").classed("ppt-link-hover", false);
        linkElmt.select("text").classed("ppt-link-hover", false);
    }
};

cypherviewer.clickSpan = function () {
    var clickedSpan = d3__namespace.select(this).data()[0];

    if (clickedSpan.node) {
        var nodeElmt = graph$1.svg.selectAll("#" + graph$1.node.gID + " > g").filter(function (d) {
            return d === clickedSpan.node;
        });

        nodeElmt.on("click").call(nodeElmt.node(), clickedSpan.node);
    }
};

cypherviewer.rightClickSpan = function () {
    var clickedSpan = d3__namespace.select(this).data()[0];

    if (clickedSpan.node) {
        var nodeElmt = graph$1.svg.selectAll("#" + graph$1.node.gID + " > g").filter(function (d) {
            return d === clickedSpan.node;
        });

        nodeElmt.on("contextmenu").call(nodeElmt.node(), clickedSpan.node);
    }
};

exports.appendFittedText = appendFittedText;
exports.cypherviewer = cypherviewer;
exports.dataModel = dataModel;
exports.graph = graph$1;
exports.logger = logger;
exports.provider = provider$1;
exports.query = query;
exports.queryviewer = queryviewer;
exports.result = result;
exports.runner = runner;
exports.start = start;
exports.taxonomy = taxonomy;
exports.tools = tools;
exports.update = update;
exports.updateGraph = updateGraph;
exports.version = version;