Reference Source

src/viewer/metadata/MetaScene.js

import {MetaModel} from "./MetaModel.js";
import {MetaObject} from "./MetaObject.js";
import {PropertySet} from "./PropertySet.js";
import {math} from "../scene/math/math.js";

/**
 * @desc Metadata corresponding to a {@link Scene}.
 *
 * * Located in {@link Viewer#metaScene}.
 * * Contains {@link MetaModel}s and {@link MetaObject}s.
 * * [Scene graph example with metadata](http://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneGraph_metadata)
 */
class MetaScene {

    /**
     * @private
     */
    constructor(viewer, scene) {

        /**
         * The {@link Viewer}.
         * @property viewer
         * @type {Viewer}
         */
        this.viewer = viewer;

        /**
         * The {@link Scene}.
         * @property scene
         * @type {Scene}
         */
        this.scene = scene;

        /**
         * The {@link MetaModel}s belonging to this MetaScene, each mapped to its {@link MetaModel#modelId}.
         *
         * @type {{String:MetaModel}}
         */
        this.metaModels = {};

        /**
         * The {@link PropertySet}s belonging to this MetaScene, each mapped to its {@link PropertySet#id}.
         *
         * @type {{String:PropertySet}}
         */
        this.propertySets = {};

        /**
         * The {@link MetaObject}s belonging to this MetaScene, each mapped to its {@link MetaObject#id}.
         *
         * @type {{String:MetaObject}}
         */
        this.metaObjects = {};

        /**
         * The {@link MetaObject}s belonging to this MetaScene, each mapped to its {@link MetaObject#type}.
         *
         * @type {{String:MetaObject}}
         */
        this.metaObjectsByType = {};

        /**
         * The root {@link MetaObject}s belonging to this MetaScene, each mapped to its {@link MetaObject#id}.
         *
         * @type {{String:MetaObject}}
         */
        this.rootMetaObjects = {};

        /**
         * Subscriptions to events sent with {@link fire}.
         * @private
         */
        this._eventSubs = {};
    }

    /**
     * Subscribes to an event fired at this Viewer.
     *
     * @param {String} event The event
     * @param {Function} callback Callback fired on the event
     */
    on(event, callback) {
        let subs = this._eventSubs[event];
        if (!subs) {
            subs = [];
            this._eventSubs[event] = subs;
        }
        subs.push(callback);
    }

    /**
     * Fires an event at this Viewer.
     *
     * @param {String} event Event name
     * @param {Object} value Event parameters
     */
    fire(event, value) {
        const subs = this._eventSubs[event];
        if (subs) {
            for (let i = 0, len = subs.length; i < len; i++) {
                subs[i](value);
            }
        }
    }

    /**
     * Unsubscribes from an event fired at this Viewer.
     * @param event
     */
    off(event) { // TODO

    }

    /**
     * Creates a {@link MetaModel} in this MetaScene.
     *
     * The MetaModel will contain a hierarchy of {@link MetaObject}s, created from the
     * meta objects in ````metaModelData````.
     *
     * The meta object hierarchy in ````metaModelData```` is expected to be non-cyclic, with a single root. If the meta
     * objects are cyclic, then this method will log an error and attempt to recover by creating a dummy root MetaObject
     * of type "Model" and connecting all other MetaObjects as its direct children. If the meta objects contain multiple
     * roots, then this method similarly attempts to recover by creating a dummy root MetaObject of type "Model" and
     * connecting all the root MetaObjects as its children.
     *
     * @param {String} modelId ID for the new {@link MetaModel}, which will have {@link MetaModel#id} set to this value.
     * @param {Object} metaModelData Data for the {@link MetaModel}.
     * @param {Object} [options] Options for creating the {@link MetaModel}.
     * @param {Boolean} [options.globalizeObjectIds=false] Whether to globalize each {@link MetaObject#id}. Set this ````true```` when you need to load multiple instances of the same meta model, to avoid ID clashes between the meta objects in the different instances.
     * @returns {MetaModel} The new MetaModel.
     */
    createMetaModel(modelId, metaModelData, options = {}) {

        const metaModel = new MetaModel({ // Registers MetaModel in #metaModels
            metaScene: this,
            id: modelId,
            projectId: metaModelData.projectId || "none",
            revisionId: metaModelData.revisionId || "none",
            author: metaModelData.author || "none",
            createdAt: metaModelData.createdAt || "none",
            creatingApplication: metaModelData.creatingApplication || "none",
            schema: metaModelData.schema || "none",
            propertySets: []
        });

        metaModel.loadData(metaModelData);

        metaModel.finalize();

        return metaModel;
    }

    /**
     * Removes a {@link MetaModel} from this MetaScene.
     *
     * Fires a "metaModelDestroyed" event with the value of the {@link MetaModel#id}.
     *
     * @param {String} metaModelId ID of the target {@link MetaModel}.
     */
    destroyMetaModel(metaModelId) {

        const metaModel = this.metaModels[metaModelId];
        if (!metaModel) {
            return;
        }

        // Remove global PropertySets

        if (metaModel.propertySets) {
            for (let i = 0, len = metaModel.propertySets.length; i < len; i++) {
                const propertySet = metaModel.propertySets[i];
                if (propertySet.metaModels.length === 1 && propertySet.metaModels[0].id === metaModelId) { // Property set owned only by this model, delete
                    delete this.propertySets[propertySet.id];
                } else {
                    const newMetaModels = [];
                    for (let j = 0, lenj = propertySet.metaModels.length; j < lenj; j++) {
                        if (propertySet.metaModels[j].id !== metaModelId) {
                            newMetaModels.push(propertySet.metaModels[j]);
                        }
                    }
                    propertySet.metaModels = newMetaModels;
                }
            }
        }

        // Remove MetaObjects

        if (metaModel.metaObjects) {
            for (let i = 0, len = metaModel.metaObjects.length; i < len; i++) {
                const metaObject = metaModel.metaObjects[i];
                const type = metaObject.type;
                const id = metaObject.id;
                if (metaObject.metaModels.length === 1&& metaObject.metaModels[0].id === metaModelId) { // MetaObject owned only by this model, delete
                    delete this.metaObjects[id];
                    if (!metaObject.parent) {
                        delete this.rootMetaObjects[id];
                    }
                }
            }
        }

        // Re-link entire MetaObject parent/child hierarchy

        for (let objectId in this.metaObjects) {
            const metaObject = this.metaObjects[objectId];
            if (metaObject.children) {
                metaObject.children = [];
            }

            // Re-link each MetaObject's property sets

            if (metaObject.propertySets) {
                metaObject.propertySets = [];
            }
            if (metaObject.propertySetIds) {
                for (let i = 0, len = metaObject.propertySetIds.length; i < len; i++) {
                    const propertySetId = metaObject.propertySetIds[i];
                    const propertySet = this.propertySets[propertySetId];
                    metaObject.propertySets.push(propertySet);
                }
            }
        }

        this.metaObjectsByType = {};

        for (let objectId in this.metaObjects) {
            const metaObject = this.metaObjects[objectId];
            const type = metaObject.type;
            if (metaObject.children) {
                metaObject.children = null;
            }
            (this.metaObjectsByType[type] || (this.metaObjectsByType[type] = {}))[objectId] = metaObject;
        }

        for (let objectId in this.metaObjects) {
            const metaObject = this.metaObjects[objectId];
            if (metaObject.parentId) {
                const parentMetaObject = this.metaObjects[metaObject.parentId];
                if (parentMetaObject) {
                    metaObject.parent = parentMetaObject;
                    (parentMetaObject.children || (parentMetaObject.children = [])).push(metaObject);
                }
            }
        }

        delete this.metaModels[metaModelId];

        // Relink MetaObjects to their MetaModels

        for (let objectId in this.metaObjects) {
            const metaObject = this.metaObjects[objectId];
            metaObject.metaModels = [];
        }

        for (let modelId in this.metaModels) {
            const metaModel = this.metaModels[modelId];
            for (let i = 0, len = metaModel.metaObjects.length; i < len; i++) {
                const metaObject = metaModel.metaObjects[i];
                metaObject.metaModels.push(metaModel);
            }
        }

        this.fire("metaModelDestroyed", metaModelId);
    }

    /**
     * Gets the {@link MetaObject#id}s of the {@link MetaObject}s that have the given {@link MetaObject#type}.
     *
     * @param {String} type The type.
     * @returns {String[]} Array of {@link MetaObject#id}s.
     */
    getObjectIDsByType(type) {
        const metaObjects = this.metaObjectsByType[type];
        return metaObjects ? Object.keys(metaObjects) : [];
    }

    /**
     * Gets the {@link MetaObject#id}s of the {@link MetaObject}s within the given subtree.
     *
     * @param {String} id  ID of the root {@link MetaObject} of the given subtree.
     * @param {String[]} [includeTypes] Optional list of types to include.
     * @param {String[]} [excludeTypes] Optional list of types to exclude.
     * @returns {String[]} Array of {@link MetaObject#id}s.
     */
    getObjectIDsInSubtree(id, includeTypes, excludeTypes) {

        const list = [];
        const metaObject = this.metaObjects[id];
        const includeMask = (includeTypes && includeTypes.length > 0) ? arrayToMap(includeTypes) : null;
        const excludeMask = (excludeTypes && excludeTypes.length > 0) ? arrayToMap(excludeTypes) : null;

        const visit = (metaObject) => {
            if (!metaObject) {
                return;
            }
            var include = true;
            if (excludeMask && excludeMask[metaObject.type]) {
                include = false;
            } else if (includeMask && (!includeMask[metaObject.type])) {
                include = false;
            }
            if (include) {
                list.push(metaObject.id);
            }
            const children = metaObject.children;
            if (children) {
                for (var i = 0, len = children.length; i < len; i++) {
                    visit(children[i]);
                }
            }
        }

        visit(metaObject);
        return list;
    }

    /**
     * Iterates over the {@link MetaObject}s within the subtree.
     *
     * @param {String} id ID of root {@link MetaObject}.
     * @param {Function} callback Callback fired at each {@link MetaObject}.
     */
    withMetaObjectsInSubtree(id, callback) {
        const metaObject = this.metaObjects[id];
        if (!metaObject) {
            return;
        }
        metaObject.withMetaObjectsInSubtree(callback);
    }
}

function arrayToMap(array) {
    const map = {};
    for (var i = 0, len = array.length; i < len; i++) {
        map[array[i]] = true;
    }
    return map;
}

export {MetaScene};