var OwlDisjointWith = require("./elements/properties/implementations/OwlDisjointWith"); var attributeParser = require("./parsing/attributeParser")(); var equivalentPropertyMerger = require("./parsing/equivalentPropertyMerger")(); var nodePrototypeMap = require("./elements/nodes/nodeMap")(); var propertyPrototypeMap = require("./elements/properties/propertyMap")(); /** * Encapsulates the parsing and preparation logic of the input data. * @param graph the graph object that will be passed to the elements * @returns {{}} */ module.exports = function ( graph ){ var parser = {}, nodes, properties, classMap, settingsData, settingsImported = false, settingsImportGraphZoomAndTranslation = false, dictionary = [], propertyMap; parser.getDictionary = function (){ return dictionary; }; parser.setDictionary = function ( d ){ dictionary = d; }; parser.settingsImported = function (){ return settingsImported; }; parser.settingsImportGraphZoomAndTranslation = function (){ return settingsImportGraphZoomAndTranslation; }; parser.parseSettings = function (){ settingsImported = true; settingsImportGraphZoomAndTranslation = false; if ( !settingsData ) { settingsImported = false; return; } /** global settings **********************************************************/ if ( settingsData.global ) { if ( settingsData.global.zoom ) { var zoomFactor = settingsData.global.zoom; graph.setZoom(zoomFactor); settingsImportGraphZoomAndTranslation = true; } if ( settingsData.global.translation ) { var translation = settingsData.global.translation; graph.setTranslation(translation); settingsImportGraphZoomAndTranslation = true; } if ( settingsData.global.paused ) { var paused = settingsData.global.paused; graph.options().pausedMenu().setPauseValue(paused); } } /** Gravity Settings **********************************************************/ if ( settingsData.gravity ) { if ( settingsData.gravity.classDistance ) { var classDistance = settingsData.gravity.classDistance; graph.options().classDistance(classDistance); } if ( settingsData.gravity.datatypeDistance ) { var datatypeDistance = settingsData.gravity.datatypeDistance; graph.options().datatypeDistance(datatypeDistance); } graph.options().gravityMenu().reset(); // reads the options values and sets the gui values } // shared variable declaration var i; var id; var checked; /** Filter Settings **********************************************************/ if ( settingsData.filter ) { // checkbox settings if ( settingsData.filter.checkBox ) { var filter_cb = settingsData.filter.checkBox; for ( i = 0; i < filter_cb.length; i++ ) { id = filter_cb[i].id; checked = filter_cb[i].checked; graph.options().filterMenu().setCheckBoxValue(id, checked); } } // node degree filter settings if ( settingsData.filter.degreeSliderValue ) { var degreeSliderValue = settingsData.filter.degreeSliderValue; graph.options().filterMenu().setDegreeSliderValue(degreeSliderValue); } graph.options().filterMenu().updateSettings(); } /** Modes Setting **********************************************************/ if ( settingsData.modes ) { // checkbox settings if ( settingsData.modes.checkBox ) { var modes_cb = settingsData.modes.checkBox; for ( i = 0; i < modes_cb.length; i++ ) { id = modes_cb[i].id; checked = modes_cb[i].checked; graph.options().modeMenu().setCheckBoxValue(id, checked); } } // color switch settings var state = settingsData.modes.colorSwitchState; // state could be undefined if ( state === true || state === false ) { graph.options().modeMenu().setColorSwitchState(state); } graph.options().modeMenu().updateSettings(); } graph.updateStyle(); // updates graph representation(setting charges and distances) }; /** * Parses the ontology data and preprocesses it (e.g. connecting inverse properties and so on). * @param ontologyData the loaded ontology json file */ parser.parse = function ( ontologyData ){ if ( !ontologyData ) { nodes = []; properties = []; dictionary = []; return; } dictionary = []; if ( ontologyData.settings ) settingsData = ontologyData.settings; else settingsData = undefined; var classes = combineClasses(ontologyData.class, ontologyData.classAttribute), datatypes = combineClasses(ontologyData.datatype, ontologyData.datatypeAttribute), combinedClassesAndDatatypes = classes.concat(datatypes), unparsedProperties = ontologyData.property || [], combinedProperties; // Inject properties for unions, intersections, ... addSetOperatorProperties(combinedClassesAndDatatypes, unparsedProperties); combinedProperties = combineProperties(unparsedProperties, ontologyData.propertyAttribute); classMap = mapElements(combinedClassesAndDatatypes); propertyMap = mapElements(combinedProperties); mergeRangesOfEquivalentProperties(combinedProperties, combinedClassesAndDatatypes); // Process the graph data convertTypesToIris(combinedClassesAndDatatypes, ontologyData.namespace); convertTypesToIris(combinedProperties, ontologyData.namespace); nodes = createNodeStructure(combinedClassesAndDatatypes, classMap); properties = createPropertyStructure(combinedProperties, classMap, propertyMap); }; /** * @return {Array} the preprocessed nodes */ parser.nodes = function (){ return nodes; }; /** * @returns {Array} the preprocessed properties */ parser.properties = function (){ return properties; }; /** * Combines the passed objects with its attributes and prototypes. This also applies * attributes defined in the base of the prototype. */ function combineClasses( baseObjects, attributes ){ var combinations = []; var prototypeMap = createLowerCasePrototypeMap(nodePrototypeMap); if ( baseObjects ) { baseObjects.forEach(function ( element ){ var matchingAttribute; if ( attributes ) { // Look for an attribute with the same id and merge them for ( var i = 0; i < attributes.length; i++ ) { var attribute = attributes[i]; if ( element.id === attribute.id ) { matchingAttribute = attribute; break; } } addAdditionalAttributes(element, matchingAttribute); } // Then look for a prototype to add its properties var Prototype = prototypeMap.get(element.type.toLowerCase()); if ( Prototype ) { addAdditionalAttributes(element, Prototype); // TODO might be unnecessary var node = new Prototype(graph); node.annotations(element.annotations) .baseIri(element.baseIri) .comment(element.comment) .complement(element.complement) .disjointUnion(element.disjointUnion) .description(element.description) .equivalents(element.equivalent) .id(element.id) .intersection(element.intersection) .label(element.label) // .type(element.type) Ignore, because we predefined it .union(element.union) .iri(element.iri); if ( element.pos ) { node.x = element.pos[0]; node.y = element.pos[1]; node.px = node.x; node.py = node.y; } //class element pin var elementPinned = element.pinned; if ( elementPinned === true ) { node.pinned(true); graph.options().pickAndPinModule().addPinnedElement(node); } // Create node objects for all individuals if ( element.individuals ) { element.individuals.forEach(function ( individual ){ var individualNode = new Prototype(graph); individualNode.label(individual.labels) .iri(individual.iri); node.individuals().push(individualNode); }); } if ( element.attributes ) { var deduplicatedAttributes = d3.set(element.attributes.concat(node.attributes())); node.attributes(deduplicatedAttributes.values()); } combinations.push(node); } else { console.error("Unknown element type: " + element.type); } }); } return combinations; } function combineProperties( baseObjects, attributes ){ var combinations = []; var prototypeMap = createLowerCasePrototypeMap(propertyPrototypeMap); if ( baseObjects ) { baseObjects.forEach(function ( element ){ var matchingAttribute; if ( attributes ) { // Look for an attribute with the same id and merge them for ( var i = 0; i < attributes.length; i++ ) { var attribute = attributes[i]; if ( element.id === attribute.id ) { matchingAttribute = attribute; break; } } addAdditionalAttributes(element, matchingAttribute); } // Then look for a prototype to add its properties var Prototype = prototypeMap.get(element.type.toLowerCase()); if ( Prototype ) { // Create the matching object and set the properties var property = new Prototype(graph); property.annotations(element.annotations) .baseIri(element.baseIri) .cardinality(element.cardinality) .comment(element.comment) .domain(element.domain) .description(element.description) .equivalents(element.equivalent) .id(element.id) .inverse(element.inverse) .label(element.label) .minCardinality(element.minCardinality) .maxCardinality(element.maxCardinality) .range(element.range) .subproperties(element.subproperty) .superproperties(element.superproperty) // .type(element.type) Ignore, because we predefined it .iri(element.iri); // adding property position if ( element.pos ) { property.x = element.pos[0]; property.y = element.pos[1]; property.px = element.pos[0]; property.py = element.pos[1]; } var elementPinned = element.pinned; if ( elementPinned === true ) { property.pinned(true); graph.options().pickAndPinModule().addPinnedElement(property); } if ( element.attributes ) { var deduplicatedAttributes = d3.set(element.attributes.concat(property.attributes())); property.attributes(deduplicatedAttributes.values()); } if ( element.instances !== undefined ) { property.instances(element.instances); } if ( element.individuals ) { element.individuals.forEach(function ( assertion ){ property.individuals().push(assertion); }); } combinations.push(property); } else { console.error("Unknown element type: " + element.type); } }); } return combinations; } function createLowerCasePrototypeMap( prototypeMap ){ return d3.map(prototypeMap.values(), function ( Prototype ){ return new Prototype().type().toLowerCase(); }); } function mergeRangesOfEquivalentProperties( properties, nodes ){ // pass clones of arrays into the merger to keep the current functionality of this module var newNodes = equivalentPropertyMerger.merge(properties.slice(), nodes.slice(), propertyMap, classMap, graph); // replace all the existing nodes and map the nodes again nodes.length = 0; Array.prototype.push.apply(nodes, newNodes); classMap = mapElements(nodes); } /** * Checks all attributes which have to be rewritten. * For example: * equivalent is filled with only ID's of the corresponding nodes. It would be better to used the * object instead of the ID so we swap the ID's with the correct object reference and can delete it from drawing * because it is not necessary. */ function createNodeStructure( rawNodes, classMap ){ var nodes = []; // Set the default values var maxIndividualCount = 0; rawNodes.forEach(function ( node ){ maxIndividualCount = Math.max(maxIndividualCount, node.individuals().length); node.visible(true); }); rawNodes.forEach(function ( node ){ // Merge and connect the equivalent nodes processEquivalentIds(node, classMap); attributeParser.parseClassAttributes(node); node.maxIndividualCount(maxIndividualCount); }); // Collect all nodes that should be displayed rawNodes.forEach(function ( node ){ if ( node.visible() ) { nodes.push(node); } }); return nodes; } /** * Sets the disjoint attribute of the nodes if a disjoint label is found. * @param property */ function processDisjoints( property ){ if ( property instanceof OwlDisjointWith === false ) { return; } var domain = property.domain(), range = property.range(); // Check the domain. if ( !domain.disjointWith() ) { domain.disjointWith([]); } // Check the range. if ( !range.disjointWith() ) { range.disjointWith([]); } domain.disjointWith().push(property.range()); range.disjointWith().push(property.domain()); } /** * Connect all properties and also their sub- and superproperties. * We iterate over the rawProperties array because it is way faster than iterating * over an object and its attributes. * * @param rawProperties the properties * @param classMap a map of all classes * @param propertyMap the properties in a map */ function createPropertyStructure( rawProperties, classMap, propertyMap ){ var properties = []; // Set default values rawProperties.forEach(function ( property ){ property.visible(true); }); // Connect properties rawProperties.forEach(function ( property ){ var domain, range, domainObject, rangeObject, inverse; /* Skip properties that have no information about their domain and range, like inverse properties with optional inverse and optional domain and range attributes */ if ( (property.domain() && property.range()) || property.inverse() ) { var inversePropertyId = findId(property.inverse()); // Look if an inverse property exists if ( inversePropertyId ) { inverse = propertyMap[inversePropertyId]; if ( !inverse ) { console.warn("No inverse property was found for id: " + inversePropertyId); property.inverse(undefined); } } // Either domain and range are set on this property or at the inverse if ( typeof property.domain() !== "undefined" && typeof property.range() !== "undefined" ) { domain = findId(property.domain()); range = findId(property.range()); domainObject = classMap[domain]; rangeObject = classMap[range]; } else if ( inverse ) { // Domain and range need to be switched domain = findId(inverse.range()); range = findId(inverse.domain()); domainObject = classMap[domain]; rangeObject = classMap[range]; } else { console.warn("Domain and range not found for property: " + property.id()); } // Set the references on this property property.domain(domainObject); property.range(rangeObject); // Also set the attributes of the inverse property if ( inverse ) { property.inverse(inverse); inverse.inverse(property); // Switch domain and range inverse.domain(rangeObject); inverse.range(domainObject); } } // Reference sub- and superproperties referenceSubOrSuperProperties(property.subproperties()); referenceSubOrSuperProperties(property.superproperties()); }); // Merge equivalent properties and process disjoints. rawProperties.forEach(function ( property ){ processEquivalentIds(property, propertyMap); processDisjoints(property); attributeParser.parsePropertyAttributes(property); }); // Add additional information to the properties rawProperties.forEach(function ( property ){ // Properties of merged classes should point to/from the visible equivalent class var propertyWasRerouted = false; if ( property.domain() === undefined ) { console.warn("No Domain was found for id:" + property.id()); return; } if ( wasNodeMerged(property.domain()) ) { property.domain(property.domain().equivalentBase()); propertyWasRerouted = true; } if ( property.range() === undefined ) { console.warn("No range was found for id:" + property.id()); return; } if ( wasNodeMerged(property.range()) ) { property.range(property.range().equivalentBase()); propertyWasRerouted = true; } // But there should not be two equal properties between the same domain and range var equalProperty = getOtherEqualProperty(rawProperties, property); if ( propertyWasRerouted && equalProperty ) { property.visible(false); equalProperty.redundantProperties().push(property); } // Hide property if source or target node is hidden if ( !property.domain().visible() || !property.range().visible() ) { property.visible(false); } // Collect all properties that should be displayed if ( property.visible() ) { properties.push(property); } }); return properties; } function referenceSubOrSuperProperties( subOrSuperPropertiesArray ){ var i, l; if ( !subOrSuperPropertiesArray ) { return; } for ( i = 0, l = subOrSuperPropertiesArray.length; i < l; ++i ) { var subOrSuperPropertyId = findId(subOrSuperPropertiesArray[i]); var subOrSuperProperty = propertyMap[subOrSuperPropertyId]; if ( subOrSuperProperty ) { // Replace id with object subOrSuperPropertiesArray[i] = subOrSuperProperty; } else { console.warn("No sub-/superproperty was found for id: " + subOrSuperPropertyId); } } } function wasNodeMerged( node ){ return !node.visible() && node.equivalentBase(); } function getOtherEqualProperty( properties, referenceProperty ){ var i, l, property; for ( i = 0, l = properties.length; i < l; i++ ) { property = properties[i]; if ( referenceProperty === property ) { continue; } if ( referenceProperty.domain() !== property.domain() || referenceProperty.range() !== property.range() ) { continue; } // Check for an equal IRI, if non existent compare label and type if ( referenceProperty.iri() && property.iri() ) { if ( referenceProperty.iri() === property.iri() ) { return property; } } else if ( referenceProperty.type() === property.type() && referenceProperty.defaultLabel() === property.defaultLabel() ) { return property; } } return undefined; } /** * Generates and adds properties for links to set operators. * @param classes unprocessed classes * @param properties unprocessed properties */ function addSetOperatorProperties( classes, properties ){ function addProperties( domainId, rangeIds, operatorType ){ if ( !rangeIds ) { return; } rangeIds.forEach(function ( rangeId, index ){ var property = { id: "GENERATED-" + operatorType + "-" + domainId + "-" + rangeId + "-" + index, type: "setOperatorProperty", domain: domainId, range: rangeId }; properties.push(property); }); } classes.forEach(function ( clss ){ addProperties(clss.id(), clss.complement(), "COMPLEMENT"); addProperties(clss.id(), clss.intersection(), "INTERSECTION"); addProperties(clss.id(), clss.union(), "UNION"); addProperties(clss.id(), clss.disjointUnion(), "DISJOINTUNION"); }); } /** * Replaces the ids of equivalent nodes/properties with the matching objects, cross references them * and tags them as processed. * @param element a node or a property * @param elementMap a map where nodes/properties can be looked up */ function processEquivalentIds( element, elementMap ){ var eqIds = element.equivalents(); if ( !eqIds || element.equivalentBase() ) { return; } // Replace ids with the corresponding objects for ( var i = 0, l = eqIds.length; i < l; ++i ) { var eqId = findId(eqIds[i]); var eqObject = elementMap[eqId]; if ( eqObject ) { // Cross reference both objects eqObject.equivalents(eqObject.equivalents()); eqObject.equivalents().push(element); eqObject.equivalentBase(element); eqIds[i] = eqObject; // Hide other equivalent nodes eqObject.visible(false); } else { console.warn("No class/property was found for equivalent id: " + eqId); } } } /** * Tries to convert the type to an iri and sets it. * @param elements classes or properties * @param namespaces an array of namespaces */ function convertTypesToIris( elements, namespaces ){ elements.forEach(function ( element ){ if ( typeof element.iri() === "string" ) { element.iri(replaceNamespace(element.iri(), namespaces)); } }); } /** * Creates a map by mapping the array with the passed function. * @param array the array * @returns {{}} */ function mapElements( array ){ var map = {}; for ( var i = 0, length = array.length; i < length; i++ ) { var element = array[i]; map[element.id()] = element; } return map; } /** * Adds the attributes of the additional object to the base object, but doesn't * overwrite existing ones. * * @param base the base object * @param addition the object with additional data * @returns the combination is also returned */ function addAdditionalAttributes( base, addition ){ // Check for an undefined value addition = addition || {}; for ( var addAttribute in addition ) { // Add the attribute if it doesn't exist if ( !(addAttribute in base) && addition.hasOwnProperty(addAttribute) ) { base[addAttribute] = addition[addAttribute]; } } return base; } /** * Replaces the namespace (and the separator) if one exists and returns the new value. * @param address the address with a namespace in it * @param namespaces an array of namespaces * @returns {string} the processed address with the (possibly) replaced namespace */ function replaceNamespace( address, namespaces ){ var separatorIndex = address.indexOf(":"); if ( separatorIndex === -1 ) { return address; } var namespaceName = address.substring(0, separatorIndex); for ( var i = 0, length = namespaces.length; i < length; ++i ) { var namespace = namespaces[i]; if ( namespaceName === namespace.name ) { return namespace.iri + address.substring(separatorIndex + 1); } } return address; } /** * Looks whether the passed object is already the id or if it was replaced * with the object that belongs to the id. * @param object an id, a class or a property * @returns {string} the id of the passed object or undefined */ function findId( object ){ if ( !object ) { return undefined; } else if ( typeof object === "string" ) { return object; } else if ( "id" in object ) { return object.id(); } else { console.warn("No Id was found for this object: " + object); return undefined; } } return parser; };