Reference Source

src/viewer/metadata/MetaModel.js

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

/**
 * @desc Metadata corresponding to an {@link Entity} that represents a model.
 *
 * An {@link Entity} represents a model when {@link Entity#isModel} is ````true````
 *
 * A MetaModel corresponds to an {@link Entity} by having the same {@link MetaModel#id} as the {@link Entity#id}.
 *
 * A MetaModel is created by {@link MetaScene#createMetaModel} and belongs to a {@link MetaScene}.
 *
 * Each MetaModel is registered by {@link MetaModel#id} in {@link MetaScene#metaModels}.
 *
 * A {@link MetaModel} represents its object structure with a tree of {@link MetaObject}s, with {@link MetaModel#rootMetaObject} referencing the root {@link MetaObject}.
 *
 * @class MetaModel
 */
class MetaModel {

    /**
     * Creates a new, unfinalized MetaModel.
     *
     * * The MetaModel is immediately registered by {@link MetaModel#id} in {@link MetaScene#metaModels}, even though it's not yet populated.
     * * The MetaModel then needs to be populated with one or more calls to {@link metaModel#loadData}.
     * * As we populate it, the MetaModel will create {@link MetaObject}s and {@link PropertySet}s in itself, and in the MetaScene.
     * * When populated, call {@link MetaModel#finalize} to finish it off, which causes MetaScene to fire a "metaModelCreated" event.
     */
    constructor(params) {

        /**
         * Globally-unique ID.
         *
         * MetaModels are registered by ID in {@link MetaScene#metaModels}.
         *
         * When this MetaModel corresponds to an {@link Entity} then this ID will match the {@link Entity#id}.
         *
         * @property id
         * @type {String|Number}
         */
        this.id = params.id;

        /**
         * The project ID
         * @property projectId
         * @type {String|Number}
         */
        this.projectId = params.projectId;

        /**
         * The revision ID, if available.
         *
         * Will be undefined if not available.
         *
         * @property revisionId
         * @type {String|Number}
         */
        this.revisionId = params.revisionId;

        /**
         * The model author, if available.
         *
         * Will be undefined if not available.
         *
         * @property author
         * @type {String}
         */
        this.author = params.author;

        /**
         * The date the model was created, if available.
         *
         * Will be undefined if not available.
         *
         * @property createdAt
         * @type {String}
         */
        this.createdAt = params.createdAt;

        /**
         * The application that created the model, if available.
         *
         * Will be undefined if not available.
         *
         * @property creatingApplication
         * @type {String}
         */
        this.creatingApplication = params.creatingApplication;

        /**
         * The model schema version, if available.
         *
         * Will be undefined if not available.
         *
         * @property schema
         * @type {String}
         */
        this.schema = params.schema;

        /**
         * Metadata on the {@link Scene}.
         *
         * @property metaScene
         * @type {MetaScene}
         */
        this.metaScene = params.metaScene;

        /**
         * The {@link PropertySet}s in this MetaModel.
         *
         * @property propertySets
         * @type  {PropertySet[]}
         */
        this.propertySets = [];

        /**
         * The root {@link MetaObject}s in this MetaModel's composition structure hierarchy.
         *
         * @property rootMetaObject
         * @type {MetaObject[]}
         */
        this.rootMetaObjects = [];

        /**
         * The {@link MetaObject}s in this MetaModel, each mapped to its ID.
         *
         * @property metaObjects
         * @type  {{String:MetaObject}}
         */
        this.metaObjects = {};

        /**
         * Connectivity graph.
         * @type {{}}
         */
        this.graph = params.graph || {};

        this.metaScene.metaModels[this.id] = this;

        this._propertyLookup = [];

        /**
         * True when this MetaModel has been finalized.
         * @type {boolean}
         */
        this.finalized = false;
    }

    /**
     * Backwards compatibility with the model having a single root MetaObject.
     *
     * @property rootMetaObject
     * @type {MetaObject|null}
     */
    get rootMetaObject() {
        if (this.rootMetaObjects.length === 1) {
            return this.rootMetaObjects[0];
        }
        return null;
    }

    /**
     * Load metamodel data into this MetaModel.
     * @param metaModelData
     */
    loadData(metaModelData, options = {}) {

        if (this.finalized) {
            throw "MetaScene already finalized - can't add more data";
        }

        this._globalizeIDs(metaModelData, options)

        const metaScene = this.metaScene;
        const propertyLookup = metaModelData.properties;

        if (propertyLookup) {
            for (let i = 0, len = propertyLookup.length; i < len; i++) {
                this._propertyLookup.push(propertyLookup[i]);
            }
        }

        // Create global Property Sets

        if (metaModelData.propertySets) {
            for (let i = 0, len = metaModelData.propertySets.length; i < len; i++) {
                const propertySetData = metaModelData.propertySets[i];
                if (!propertySetData.properties) { // HACK: https://github.com/Creoox/creoox-ifc2gltfcxconverter/issues/8
                    propertySetData.properties = [];
                }
                let propertySet = metaScene.propertySets[propertySetData.id];
                if (!propertySet) {
                    propertySet = new PropertySet({
                        id: propertySetData.id,
                        originalSystemId: propertySetData.originalSystemId || propertySetData.id,
                        type: propertySetData.type,
                        name: propertySetData.name,
                        properties: propertySetData.properties
                    });
                    metaScene.propertySets[propertySet.id] = propertySet;
                }
                propertySet.metaModels.push(this);
                this.propertySets.push(propertySet);
            }
        }

        if (metaModelData.metaObjects) {
            for (let i = 0, len = metaModelData.metaObjects.length; i < len; i++) {
                const metaObjectData = metaModelData.metaObjects[i];
                const id = metaObjectData.id;
                let metaObject = metaScene.metaObjects[id];
                if (!metaObject) {
                    const type = metaObjectData.type;
                    const originalSystemId = metaObjectData.originalSystemId;
                    const propertySetIds = metaObjectData.propertySets || metaObjectData.propertySetIds;
                    metaObject = new MetaObject({
                        id,
                        originalSystemId,
                        parentId: metaObjectData.parent,
                        type,
                        name: metaObjectData.name,
                        attributes: metaObjectData.attributes,
                        propertySetIds,
                        external: metaObjectData.external,
                    });
                    this.metaScene.metaObjects[id] = metaObject;
                    metaObject.metaModels = [];
                }
                this.metaObjects[id] =metaObject;
                if (!metaObjectData.parent) {
                    this.rootMetaObjects.push(metaObject);
                    metaScene.rootMetaObjects[id] = metaObject;
                }
            }
        }
    }

    _decompressProperties(propertyLookup, properties) {
        const propsNotFound = [];
        for (let i = 0, len = properties.length; i < len; i++) {
            const property = properties[i];
            if (Number.isInteger(property)) {
                const lookupProperty = propertyLookup[property];
                if (lookupProperty) {
                    properties[i] = lookupProperty;
                } else {
                    propsNotFound.push(property);
                }
            }
        }
        if (propsNotFound.length > 0) {
            console.error(`[MetaModel._decompressProperties] Properties not found: ${propsNotFound}`);
        }
    }

    finalize() {

        if (this.finalized) {
            throw "MetaScene already finalized - can't re-finalize";
        }

        // Re-link MetaScene's entire MetaObject parent/child hierarchy

        const metaScene = this.metaScene;

        for (let objectId in metaScene.metaObjects) {
            const metaObject = metaScene.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 = metaScene.propertySets[propertySetId];
                    metaObject.propertySets.push(propertySet);
                }
            }
        }

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

        // Relink MetaObjects to their MetaModels

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

        for (let modelId in metaScene.metaModels) {
            const metaModel = metaScene.metaModels[modelId];
            for (let objectId in metaModel.metaObjects) {
                const metaObject = metaModel.metaObjects[objectId];
                metaObject.metaModels.push(metaModel);
            }
        }

        // Rebuild MetaScene's MetaObjects-by-type lookup

        metaScene.metaObjectsByType = {};
        for (let objectId in metaScene.metaObjects) {
            const metaObject = metaScene.metaObjects[objectId];
            const type = metaObject.type;
            (metaScene.metaObjectsByType[type] || (metaScene.metaObjectsByType[type] = {}))[objectId] = metaObject;
        }

        // Decompress properties

        if (this.propertySets) {
            for (let i = 0, len = this.propertySets.length; i < len; i++) {
                const propertySet = this.propertySets[i];
                this._decompressProperties(this._propertyLookup, propertySet.properties);
            }
        }


        this._propertyLookup = [];

        this.finalized = true;

        this.metaScene.fire("metaModelCreated", this.id);
    }

    /**
     * Gets this MetaModel as JSON.
     * @returns {{schema: (String|string|*), createdAt: (String|string|*), metaObjects: *[], author: (String|string|*), id: (String|Number|string|number|*), creatingApplication: (String|string|*), projectId: (String|Number|string|number|*), propertySets: *[]}}
     */
    getJSON() {
        const json = {
            id: this.id,
            projectId: this.projectId,
            author: this.author,
            createdAt: this.createdAt,
            schema: this.schema,
            creatingApplication: this.creatingApplication,
            metaObjects: [],
            propertySets: []
        };
        const metaObjectsList = Object.values(this.metaObjects);
        for (let i = 0, len = metaObjectsList.length; i < len; i++) {
            const metaObject = metaObjectsList[i];
            const metaObjectCfg = {
                id: metaObject.id,
                originalSystemId: metaObject.originalSystemId,
                extId: metaObject.extId,
                type: metaObject.type,
                name: metaObject.name
            };
            if (metaObject.parent) {
                metaObjectCfg.parent = metaObject.parent.id;
            }
            if (metaObject.attributes) {
                metaObjectCfg.attributes = metaObject.attributes;
            }
            if (metaObject.propertySetIds) {
                metaObjectCfg.propertySetIds = metaObject.propertySetIds;
            }
            json.metaObjects.push(metaObjectCfg);
        }
        for (let i = 0, len = this.propertySets.length; i < len; i++) {
            const propertySet = this.propertySets[i];
            const propertySetCfg = {
                id: propertySet.id,
                originalSystemId: propertySet.originalSystemId,
                extId: propertySet.extId,
                type: propertySet.type,
                name: propertySet.name,
                propertyies: []
            };
            for (let j = 0, lenj = propertySet.properties.length; j < lenj; j++) {
                const property = propertySet.properties[j];
                const propertyCfg = {
                    id: property.id,
                    description: property.description,
                    type: property.type,
                    name: property.name,
                    value: property.value,
                    valueType: property.valueType
                };
                propertySetCfg.properties.push(propertyCfg);
            }
            json.propertySets.push(propertySetCfg);
        }
        return json;
    }

    _globalizeIDs(metaModelData, options) {

        const globalize = !!options.globalizeObjectIds;

        if (metaModelData.metaObjects) {
            for (let i = 0, len = metaModelData.metaObjects.length; i < len; i++) {
                const metaObjectData = metaModelData.metaObjects[i];

                // Globalize MetaObject IDs and parent IDs

                metaObjectData.originalSystemId = metaObjectData.id;
                if (metaObjectData.parent) {
                    metaObjectData.originalParentSystemId = metaObjectData.parent;
                }
                if (globalize) {
                    metaObjectData.id = math.globalizeObjectId(this.id, metaObjectData.id);
                    if (metaObjectData.parent) {
                        metaObjectData.parent = math.globalizeObjectId(this.id, metaObjectData.parent);
                    }
                }

                // Globalize MetaObject property set IDs

                if (globalize) {
                    const propertySetIds = metaObjectData.propertySetIds;
                    if (propertySetIds) {
                        const propertySetGlobalIds = [];
                        for (let j = 0, lenj = propertySetIds.length; j < lenj; j++) {
                            propertySetGlobalIds.push(math.globalizeObjectId(this.id, propertySetIds[j]));
                        }
                        metaObjectData.propertySetIds = propertySetGlobalIds;
                        metaObjectData.originalSystemPropertySetIds = propertySetIds;
                    }
                } else {
                    metaObjectData.originalSystemPropertySetIds = metaObjectData.propertySetIds;
                }
            }
        }

        // Globalize global PropertySet IDs

        if (metaModelData.propertySets) {
            for (let i = 0, len = metaModelData.propertySets.length; i < len; i++) {
                const propertySet = metaModelData.propertySets[i];
                propertySet.originalSystemId = propertySet.id;
                if (globalize) {
                    propertySet.id = math.globalizeObjectId(this.id, propertySet.id);
                }
            }
        }
    }
}


export {MetaModel};