lib/modelToText/generateGraph.js

Summary

Maintainability
F
4 days
Test Coverage
//////////////////////////////////////////
// Requirements                         //
//////////////////////////////////////////

var fs         = require('fs-extra');
var Promise    = require('bluebird');
var _          = require('lodash');
var semlog     = require('semlog');
var log        = semlog.log;

/**
 * 1)   Builds a graph model from the current development data model
 *      This takes the relationships between the fields, models and forms into account.
 *      The resulting graph will be exported as a .gexf (Gephi) file.
 *      The .gefx file needs further layouting to be ready for viewing in the interactive graph browser!
 *
 * 2)   Validates the model for consistency
 *      This takes the relationships into account and will throw warnings if inconsistencies are detected
 *
 * @param settings
 * @param registry
 */
exports.exec = function(settings, registry) {

    'use strict';

    registry.graph = {
        nodes: {},
        edges: {},
        connections: 0
    };

    /** Settings */
    exports.settings = settings;

    /** Map, containing all nodes */
    exports.nodes = registry.graph.nodes;

    /** Map, containing all edges */
    exports.edges = registry.graph.edges;

    return new Promise(function(resolve) {

        if (!settings.buildGraph) {
            return resolve(registry);
        }

        //////////////////////////////////////////
        // Analyze Expanded Registry            //
        //////////////////////////////////////////

        exports.analyzeFields(registry.expandedField);
        exports.analyzeModels(registry.expandedModel, registry.expandedField);
        exports.analyzeForms(registry.expandedForm);


        //////////////////////////////////////////
        // Analyze Templates                    //
        //////////////////////////////////////////

        exports.analyzeTemplates(registry.smw_template);


        //////////////////////////////////////////
        // Export to .gefx (Gephi)              //
        //////////////////////////////////////////

        var gefxExport = exports.gefxExport(exports.nodes, exports.edges);
        fs.outputFile(settings.processedModelDir + '_graph.gexf', gefxExport);

        resolve(registry);

    });

};

/**
 * Analyzes the fields of the model
 * Fields are always nodes
 *
 * @param fields
 */
exports.analyzeFields = function(fields) {
    'use strict';

    for (var fieldName in fields) {

        // Create DataType Nodes
        var field = fields[fieldName];
        var fieldPageName = 'property:' + fieldName;

        // Ignore abstract and ignored fields
        if (field.$abstract || field.$ignore) {
            continue;
        }

        var node = {
            id: fieldPageName,
            label: fieldPageName,
            niceLabel: field.title || fieldPageName,
            filepath: field.$filepath || '',
            description: field.description || '',
            size: exports.settings.buildGraphSettings.dataTypeNodeSize,
            connections: 0
        };

        var inspect = field.items || field;

        node.type = inspect.type || false;
        var format = inspect.format || false;

        node.color = exports.getColor(node.type);

        if (field.items) {
            field.multiple = true;
        }

        if (inspect.form) {

            // Calculate and write field target
            // TODO:
            field.target = inspect.form[0];

        } else {

            if (format) {
                node.type += '-' + format;
            }

            field.target = fieldName;
            exports.nodes[fieldPageName] = node;
        }
    }
};

/**
 * Adds models to the graph and their relationships to fields
 *
 * @param models
 * @param fields
 */
exports.analyzeModels = function(models, fields) {
    'use strict';

    var model;
    var modelPageName;

    // Iterate Models to create Model Nodes
    for (var modelName in models) {

        model = models[modelName];
        modelPageName = 'template:' + modelName;

        // Ignore abstract and ignored models
        if (model.$abstract || model.$ignore) {
            continue;
        }

        exports.nodes[modelPageName] = {
            id: modelPageName,
            label: modelPageName,
            niceLabel: model.title || modelPageName,
            filepath: model.$filepath || '',
            description: model.description || '',
            type: 'Model',
            color: exports.getColor('Model'),
            size: exports.settings.buildGraphSettings.modelNodeSize,
            connections: 0
        };

        if (model.smw_subobject) {
            exports.nodes[modelPageName].type = 'SubObject';
            exports.color = exports.getColor('SubObject');
        }

    }

    // Iterate Models to generate edges, depending on the fields used
    for (modelName in models) {

        model = models[modelName];
        modelPageName = 'template:' + modelName;

        // Ignore abstract and ignored models
        if (model.$abstract || model.$ignore) {
            continue;
        }

        for (var propertyName in model.properties) {

            var property = model.properties[propertyName];

            // If no $reference object is attached, something went wrong when building the registry
            // Give some more detailed informations on this.
            if (!property.$reference || !property.$reference.id) {
                log('[E] [Graph] "' + modelPageName + '" is malformed, missing its refenence, or ID!');
                log(property);
                continue;
            }

            var fieldName = property.$reference.id;
            var fieldPageName = 'property:' + fieldName;
            var field = fields[fieldName];

            var edge;

            if (field) {

                var edgeTemplate =  {
                    undirectedId: field.id,
                    label: field.title,
                    source: modelPageName,
                    weight: exports.settings.buildGraphSettings.edgeWeight
                };

                var inspect = field.items || field;

                // anyOf || allOf
                if (inspect.form) {

                    for (var i = 0; i < inspect.form.length; i++) {

                        edge = _.cloneDeep(edgeTemplate);

                        var target = inspect.form[i];

                        edge.target = 'template:' + target;
                        edge.id = modelPageName + '-' + edge.target;

                        if (field.items) {
                            edge.weight = exports.settings.buildGraphSettings.multipleEdgeWeight;
                        }

                        if (!exports.edges[edge.id]) {
                            exports.edges[edge.id] = edge;
                        }
                    }

                } else if (exports.nodes[modelPageName]) {

                    edge = _.cloneDeep(edgeTemplate);

                    // Checks if first char is uppercase. If it is, it references to a Template
                    // If not, it references to an attribute (which is represented as a helper node)
                    // TODO: This does not work if a form does not have a corresponding model with the same name

                    if (!field.target) {
                        log('[E] [Graph] Field "' + fieldPageName + '" has undefined target!');
                        log(field);
                    } else if (field.target[0] === field.target[0].toUpperCase()) {
                        edge.target = 'template:' + field.target;
                    } else {
                        edge.target = 'property:' + field.target;
                    }

                    edge.id = modelPageName + '-' + edge.target;

                    if (field.items) {
                        edge.weight = exports.settings.buildGraphSettings.multipleEdgeWeight;
                    }

                    if (!exports.edges[edge.id]) {
                        exports.edges[edge.id] = edge;
                    }
                }
            }
        }
    }
};

/**
 * Adds forms to the graph
 *
 * @param forms
 */
exports.analyzeForms = function(forms) {
    'use strict';

    for (var formName in forms) {

        var form = forms[formName];
        var id = form.id || formName;
        var formPageName = 'form:' + id;

        if (form.$abstract || form.$ignore) {
            continue;
        }

        exports.nodes[formPageName] = {
            id: formPageName,
            label: formPageName,
            niceLabel: form.title || formPageName,
            filepath: form.$filepath || '',
            description: form.description || '',
            type: 'Form',
            color: exports.getColor('Form'),
            size: exports.settings.buildGraphSettings.formNodeSize,
            connections: 0
        };

        var properties = form.properties;

        if (properties) {

            for (var modelName in properties) {

                var model = properties[modelName];

                if (model) {
                    var modelId = model.id  || modelName;
                    if (model.items) {
                        modelId = model.items.id;
                    }

                    var modelPageName = 'template:' + modelId;

                    if (modelId) {
                        var edgeId = formPageName + '-' + modelName;

                        // Ignore wikitext attributes, since they are directly written into the form template
                        if (!exports.edges[edgeId] && !model.wikitext) {
                            exports.edges[edgeId] = {
                                id: edgeId,
                                undirectedId: 'smwTemplate',
                                label: 'smwTemplate',
                                source: formPageName,
                                target: modelPageName,
                                weight: exports.settings.buildGraphSettings.edgeWeight
                            };
                        }
                    }
                }
            }
        }
    }
};

/**
 * Adds the wikitext mobo_template
 *
 * @param templates
 */
exports.analyzeTemplates = function(templates) {
    'use strict';

    // Iterate Models to create Model Nodes
    for (var templateName in templates) {

        var templatePageName = 'template:' + templateName.replace('.wikitext', '');

        if (!exports.nodes[templatePageName]) {
            exports.nodes[templatePageName] = {
                id: templatePageName,
                label: templatePageName,
                niceLabel: templatePageName,
                filepath: '',
                description: '',
                type: 'Template',
                color: exports.getColor('Template'),
                size: exports.settings.buildGraphSettings.templateNodeSize,
                connections: 0
            };
        }
    }
};

/**
 * GEFX is the Gephi Graph format.
 * It can be imported and layouted there.
 *
 * @param nodes
 * @param edges
 * @returns {string}
 */
exports.gefxExport = function(nodes, edges) {
    'use strict';

    var node;
    var edge;
    var nodeId;
    var edgeId;

    var gefxExport = '\ufeff<?xml version="1.0" encoding="UTF-8"?>\n';
    gefxExport    += '<gexf xmlns="http://www.gexf.net/1.2draft" version="1.2" xmlns:viz="http://www.gexf.net/1.2draft/viz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gexf.net/1.2draft http://www.gexf.net/1.2draft/gexf.xsd">\n';
    gefxExport    += '  <meta lastmodifieddate="2014-06-04">\n';
    gefxExport    += '      <creator>Simon Heimler</creator>\n';
    gefxExport    += '      <description>mobo graph visualizaton, generated by https://github.com/Fannon/mobo</description>\n';
    gefxExport    += '  </meta>\n';
    gefxExport    += '  <graph mode="static" defaultedgetype="directed">\n';
    gefxExport    += '      <attributes class="node">\n';
    gefxExport    += '          <attribute id="type" title="Type" type="string"/>\n';
    gefxExport    += '          <attribute id="size" title="Size" type="integer"/>\n';
    gefxExport    += '          <attribute id="nicelabel" title="Nice Label" type="string"/>\n';
    gefxExport    += '          <attribute id="filepath" title="File path" type="string"/>\n';
    gefxExport    += '          <attribute id="description" title="Description" type="string"/>\n';
    gefxExport    += '      </attributes>\n';
    gefxExport    += '      <attributes class="edge">\n';
    gefxExport    += '          <attribute id="undirectedId" title="Undirected ID" type="string"/>\n';
    gefxExport    += '      </attributes>\n';


    //////////////////////////////////////////
    // Export: Nodes                        //
    //////////////////////////////////////////

    gefxExport    += '      <nodes>\n';

    for (nodeId in nodes) {
        node = nodes[nodeId];
        gefxExport    += '          <node id="' + node.id + '" label="' + exports.escape(node.label) + '">\n';
        gefxExport    += '              <attvalues>\n';
        gefxExport    += '                  <attvalue for="type" value="' + node.type + '"/>\n';
        gefxExport    += '                  <attvalue for="size" value="' + node.size + '"/>\n';
        gefxExport    += '                  <attvalue for="nicelabel" value="' + exports.escape(node.niceLabel) + '"/>\n';
        gefxExport    += '                  <attvalue for="filepath" value="' + exports.escape(node.filepath) + '"/>\n';
        gefxExport    += '                  <attvalue for="description" value="' + exports.escape(node.description) + '"/>\n';
        gefxExport    += '              </attvalues>\n';
        gefxExport    += '              <viz:size value="' + node.size + '"></viz:size>\n';
        gefxExport    += '              <viz:color ' + node.color + '></viz:color>\n';
        gefxExport    += '          </node>\n';
    }
    gefxExport    += '      </nodes>\n';

    //////////////////////////////////////////
    // Export: Edges                        //
    //////////////////////////////////////////

    gefxExport    += '      <edges>\n';
    for (edgeId in edges) {
        edge = edges[edgeId];
        gefxExport    += '          <edge id="' + edge.id + '" label="' + edge.label + '" source="' + edge.source + '" target="' + edge.target + '" weight="' + edge.weight + '">\n';
        gefxExport    += '              <attvalues>\n';
        gefxExport    += '                  <attvalue for="undirectedId" value="' + edge.undirectedId + '"/>\n';
        gefxExport    += '              </attvalues>\n';
        gefxExport    += '          </edge>\n';
    }
    gefxExport    += '      </edges>\n';


    //////////////////////////////////////////
    // Export: Footer                       //
    //////////////////////////////////////////

    gefxExport    += '  </graph>\n';
    gefxExport    += '</gexf>\n';

    return gefxExport;
};


//////////////////////////////////////////
// HELPER FUNCTIONS                     //
//////////////////////////////////////////

/**
 * Escapes certain characters for the XML output
 *
 * @param string
 * @returns {string}    escaped string
 */
exports.escape = function(string) {
    'use strict';
    if (string && string.replace) {
        return string.replace('&', '&amp;');
    }
};

/**
 * Colors the node depending on their function
 *
 * @param type
 * @returns {string}
 */
exports.getColor = function(type) {
    'use strict';
    if (type) {
        if (type === 'Form') {
            return 'r="73" g="199" b="232"';
        } else if (type === 'Model') {
            return 'r="168" g="242" b="53"';
        } else if (type === 'SubObject') {
            return 'r="226" g="242" b="87"';
        } else if (type === 'Template') {
            return 'r="250" g="189" b="43"';
        } else if (type === 'array') {
            return 'r="125" g="235" b="143"';
        } else if (type === 'number') {
            return 'r="209" g="110" b="244"';
        } else if (type.indexOf('string') > -1) {
            return 'r="149" g="91" b="244"';
        } else if (type === 'boolean') {
            return 'r="244" g="91" b="91"';
        }
    }

    // Use grey if nothing could be applied
    return 'r="165" g="165" b="165"';

};