Reference Source

src/plugins/TreeViewPlugin/TreeViewPlugin.js

import {Plugin} from "../../viewer/Plugin.js";
import {RenderService} from "./RenderService.js";

const treeViews = [];

/**
 * @desc A {@link Viewer} plugin that provides an HTML tree view to navigate the IFC elements in models.
 * <br>
 *
 * <a href="https://xeokit.github.io/xeokit-sdk/examples/#BIMOffline_XKT_WestRiverSideHospital" style="border: 1px solid black;"><img src="http://xeokit.io/img/docs/TreeViewPlugin/TreeViewPlugin.png"></a>
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/#BIMOffline_XKT_WestRiverSideHospital)]
 *
 * ## Overview
 *
 * * A fast HTML tree view, with zero external dependencies, that works with huge numbers of objects.
 * * Each tree node has a checkbox to control the visibility of its object.
 * * Has three hierarchy modes: "containment", "types" and "storeys".
 * * Automatically contains all models (that have metadata) that are currently in the {@link Scene}.
 * * Sorts tree nodes by default - spatially, from top-to-bottom for ````IfcBuildingStorey```` nodes, and alphanumerically for other nodes.
 * * Allows custom CSS styling.
 * * Use {@link ContextMenu} to create a context menu for the tree nodes.
 *
 * ## Credits
 *
 * TreeViewPlugin is based on techniques described in [*Super Fast Tree View in JavaScript*](https://chrissmith.xyz/super-fast-tree-view-in-javascript/) by [Chris Smith](https://twitter.com/chris22smith).
 *
 * ## Usage
 *
 * In the example below, we'll add a TreeViewPlugin which, by default, will automatically show the structural
 * hierarchy of the IFC elements in each model we load.
 *
 * Then we'll use an {@link XKTLoaderPlugin} to load the Schependomlaan model from an
 * [.xkt file](https://github.com/xeokit/xeokit-sdk/tree/master/examples/models/xkt/schependomlaan).
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/#BIMOffline_XKT_Schependomlaan)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, TreeViewPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas",
 *      transparent: true
 * });
 *
 * viewer.camera.eye = [-2.56, 8.38, 8.27];
 * viewer.camera.look = [13.44, 3.31, -14.83];
 * viewer.camera.up = [0.10, 0.98, -0.14];
 *
 * const treeView = new TreeViewPlugin(viewer, {
 *     containerElement: document.getElementById("myTreeViewContainer")
 * });
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *     id: "myModel",
 *     src: "./models/xkt/Schependomlaan.xkt",
 *     edges: true
 * });
 * ````
 *
 * ## Manually Adding Models
 *
 * Instead of adding models automatically, we can control which models appear in our TreeViewPlugin by adding them manually.
 *
 * In the next example, we'll configure the TreeViewPlugin to not add models automatically. Then, once the model
 * has loaded, we'll add it manually using {@link TreeViewPlugin#addModel}.
 *
 * ````javascript
 * const treeView = new TreeViewPlugin(viewer, {
 *      containerElement: document.getElementById("myTreeViewContainer"),
 *      autoAddModels: false  // <<---------------- Don't auto-add models
 * });
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *     id: "myModel",
 *     src: "./models/xkt/Schependomlaan.xkt",
 *     edges: true
 * });
 *
 * model.on("loaded", () => {
 *      treeView.addModel(model.id);
 * });
 * ````
 *
 * Adding models manually also allows us to set some options for the model. For example, the ````rootName```` option allows us to provide a custom name for
 * the root node, which is sometimes desirable when the model's "IfcProject" element's name is not suitable:
 *
 * ````javascript
 * model.on("loaded", () => {
 *      treeView.addModel(model.id, {
 *          rootName: "Schependomlaan Model"
 *      });
 * });
 * ````
 *
 * ## Initially Expanding the Hierarchy
 *
 * We can also configure TreeViewPlugin to initially expand each model's nodes to a given depth.
 *
 * Let's automatically expand the first three nodes from the root, for every model added:
 *
 * ````javascript
 * const treeView = new TreeViewPlugin(viewer, {
 *      containerElement: document.getElementById("myTreeViewContainer"),
 *      autoExpandDepth: 3
 * });
 * ````
 *
 * ## Showing a Node by ID
 *
 * We can show a given node using its ID. This causes the TreeViewPlugin to collapse, then expand and scroll the node into view, then highlight the node.
 *
 * See the documentation for the {@link TreeViewPlugin#showNode} method for more information, including how to define a custom highlighted appearance for the node using CSS.
 *
 * Let's make the TreeViewPlugin show the node corresponding to whatever object {@link Entity} that we pick:
 *
 * ````javascript
 * viewer.cameraControl.on("picked", function (e) {
 *     var objectId = e.entity.id;
 *     treeView.showNode(objectId);
 * });
 * ````
 *
 * This will de-highlight any node that was previously shown by this method.
 *
 * Note that this method only works if the picked {@link Entity} is an object that belongs to a model that's represented in the TreeViewPlugin.
 *
 * ## Customizing Appearance
 *
 * We can customize the appearance of our TreeViewPlugin by defining custom CSS for its HTML
 * elements. See our example's [source code](https://github.com/xeokit/xeokit-sdk/blob/master/examples/BIMOffline_XKT_Schependomlaan.html)
 * for an example of custom CSS rules.
 *
 * ## Model Hierarchies
 *
 * TreeViewPlugin has three hierarchies for organizing its nodes:
 *
 * * "containment" - organizes the tree nodes to indicate the containment hierarchy of the {@link MetaObject}s.
 * * "types" - groups nodes by their IFC types.
 * * "storeys" - groups nodes within their ````IfcBuildingStoreys````, and sub-groups them by their IFC types.
 *
 * <br>
 * The table below shows what the hierarchies look like:
 * <br>
 *
 * | 1. Containment Hierarchy | 2. Types Hierarchy | 3. Storeys Hierarchy |
 * |---|---|---|
 * | <img src="http://xeokit.io/img/docs/TreeViewPlugin/structureMode.png"> | <img src="http://xeokit.io/img/docs/TreeViewPlugin/typesMode.png"> | <img src="http://xeokit.io/img/docs/TreeViewPlugin/storeysMode.png"> |
 * <br>
 *
 * Let's create a TreeViewPlugin that groups nodes by their building stories and IFC types:
 *
 * ````javascript
 * const treeView = new TreeViewPlugin(viewer, {
 *      containerElement: document.getElementById("myTreeViewContainer"),
 *      hierarchy: "stories"
 * });
 * ````
 *
 * ## Sorting Nodes
 *
 * TreeViewPlugin sorts its tree nodes by default. For a "storeys" hierarchy, it orders ````IfcBuildingStorey```` nodes
 * spatially, with the node for the highest story at the top, down to the lowest at the bottom.
 *
 * For all the hierarchy types ("containment", "classes" and "storeys"), TreeViewPlugin sorts the other node types
 * alphanumerically on their titles.
 *
 * If for some reason you need to prevent sorting, create your TreeViewPlugin with the option disabled, like so:
 *
 * ````javascript
 * const treeView = new TreeViewPlugin(viewer, {
 *      containerElement: document.getElementById("myTreeViewContainer"),
 *      hierarchy: "stories",
 *      sortNodes: false // <<------ Disable node sorting
 * });
 * ````
 *
 * Note that, for all hierarchy modes, node sorting is only done for each model at the time that it is added to the TreeViewPlugin, and will not
 * update dynamically if we later transform the {@link Entity}s corresponding to the nodes.
 *
 * ## Pruning empty nodes
 *
 * Sometimes a model contains subtrees of objects that don't have any geometry. These are models whose
 * {@link MetaModel} contains trees of {@link MetaObject}s that don't have any {@link Entity}s in the {@link Scene}.
 *
 * For these models, the tree view would contain nodes that don't do anything in the Scene when we interact with them,
 * which is undesirable.
 *
 * By default, TreeViewPlugin will not create nodes for those objects. However, we can override that behaviour if we want
 * to have nodes for those objects (perhaps for debugging the model):
 *
 * ````javascript
 * const treeView = new TreeViewPlugin(viewer, {
 *      containerElement: document.getElementById("myTreeViewContainer"),
 *      hierarchy: "stories",
 *      pruneEmptyNodes: false // <<------ Create nodes for object subtrees without geometry
 * });
 * ````
 *
 * ## Context Menu
 *
 * TreeViewPlugin fires a "contextmenu" event whenever we right-click on a tree node.
 *
 * The event contains:
 *
 * * ````event```` - the original [contextmenu](https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event) [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)
 * * ````viewer```` - the {@link Viewer}
 * * ````treeViewPlugin```` - the TreeViewPlugin
 * * ````treeViewNode```` - the {@link TreeViewNode} representing the tree node
 *<br><br>
 *
 * Let's use {@link ContextMenu} to show a simple context menu for the node we clicked.
 *
 * [[Run an example](https://xeokit.github.io/xeokit-sdk/examples/#ContextMenu_Canvas_TreeViewPlugin_Custom)]
 *
 * ````javascript
 * import {ContextMenu} from "../src/extras/ContextMenu/ContextMenu.js";
 *
 * const treeViewContextMenu = new ContextMenu({
 *     items: [
 *         [
 *             [
 *                 {
 *                     title: "Hide",
 *                     doAction: function (context) {
 *                         context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
 *                             if (treeViewNode.objectId) {
 *                                 const entity = context.viewer.scene.objects[treeViewNode.objectId];
 *                                 if (entity) {
 *                                     entity.visible = false;
 *                                 }
 *                             }
 *                         });
 *                     }
 *                 },
 *                 {
 *                     title: "Hide all",
 *                     doAction: function (context) {
 *                         context.viewer.scene.setObjectsVisible(context.viewer.scene.visibleObjectIds, false);
 *                     }
 *                 }
 *             ],
 *             [
 *                 {
 *                     title: "Show",
 *                     doAction: function (context) {
 *                         context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
 *                             if (treeViewNode.objectId) {
 *                                 const entity = context.viewer.scene.objects[treeViewNode.objectId];
 *                                 if (entity) {
 *                                     entity.visible = true;
 *                                     entity.xrayed = false;
 *                                     entity.selected = false;
 *                                 }
 *                             }
 *                         });
 *                     }
 *                 },
 *                 {
 *                     title: "Show all",
 *                     doAction: function (context) {
 *                         const scene = context.viewer.scene;
 *                         scene.setObjectsVisible(scene.objectIds, true);
 *                         scene.setObjectsXRayed(scene.xrayedObjectIds, false);
 *                         scene.setObjectsSelected(scene.selectedObjectIds, false);
 *                     }
 *                 }
 *             ]
 *         ]
 *     ]
 * });
 *
 * treeView.on("contextmenu", (e) => {
 *
 *     const event = e.event;                           // MouseEvent
 *     const viewer = e.viewer;                         // Viewer
 *     const treeViewPlugin = e.treeViewPlugin;         // TreeViewPlugin
 *     const treeViewNode = e.treeViewNode;             // TreeViewNode
 *
 *     treeViewContextMenu.show(e.event.pageX, e.event.pageY);
 *
 *     treeViewContextMenu.context = {
 *         viewer: e.viewer,
 *         treeViewPlugin: e.treeViewPlugin,
 *         treeViewNode: e.treeViewNode
 *     };
 * });
 * ````
 *
 * ## Clicking Node Titles
 *
 * TreeViewPlugin fires a "nodeTitleClicked" event whenever we left-click on a tree node.
 *
 * Like the "contextmenu" event, this event contains:
 *
 * * ````event```` - the original [click](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event) [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)
 * * ````viewer```` - the {@link Viewer}
 * * ````treeViewPlugin```` - the TreeViewPlugin
 * * ````treeViewNode```` - the {@link TreeViewNode} representing the tree node
 *<br><br>
 *
 * Let's register a callback to isolate and fit-to-view the {@link Entity}(s) represented by the node. This callback is
 * going to X-ray all the other Entitys, fly the camera to fit the Entity(s) for the clicked node, then hide the other Entitys.
 *
 * [[Run an example](https://xeokit.github.io/xeokit-sdk/examples/#ContextMenu_Canvas_TreeViewPlugin_Custom)]
 *
 * ````javascript
 * treeView.on("nodeTitleClicked", (e) => {
 *     const scene = viewer.scene;
 *     const objectIds = [];
 *     e.treeViewPlugin.withNodeTree(e.treeViewNode, (treeViewNode) => {
 *         if (treeViewNode.objectId) {
 *             objectIds.push(treeViewNode.objectId);
 *         }
 *     });
 *     scene.setObjectsXRayed(scene.objectIds, true);
 *     scene.setObjectsVisible(scene.objectIds, true);
 *     scene.setObjectsXRayed(objectIds, false);
 *     viewer.cameraFlight.flyTo({
 *         aabb: scene.getAABB(objectIds),
 *         duration: 0.5
 *     }, () => {
 *         setTimeout(function () {
 *             scene.setObjectsVisible(scene.xrayedObjectIds, false);
 *             scene.setObjectsXRayed(scene.xrayedObjectIds, false);
 *         }, 500);
 *     });
 * });
 * ````
 *
 * To make the cursor change to a pointer when we hover over the node titles, and also to make the titles change to blue, we'll also define this CSS for the ````<span>```` elements
 * that represent the titles of our TreeViewPlugin nodes:
 *
 * ````css
 * #treeViewContainer ul li span:hover {
 *      color: blue;
 *      cursor: pointer;
 * }
 * ````
 *
 * @class TreeViewPlugin
 */
export class TreeViewPlugin extends Plugin {

    /**
     * @constructor
     *
     * @param {Viewer} viewer The Viewer.
     * @param {*} cfg Plugin configuration.
     * @param {String} [cfg.containerElementId]  ID of an existing HTML element to contain the TreeViewPlugin - either this or containerElement is mandatory. When both values are given, the element reference is always preferred to the ID.
     * @param {HTMLElement} cfg.containerElement DOM element to contain the TreeViewPlugin.
     * @param {Boolean} [cfg.autoAddModels=true] When ````true```` (default), will automatically add each model as it's created. Set this ````false```` if you want to manually add models using {@link TreeViewPlugin#addModel} instead.
     * @param {Number} [cfg.autoExpandDepth] Optional depth to which to initially expand the tree.
     * @param {String} [cfg.hierarchy="containment"] How to organize the tree nodes: "containment", "storeys" or "types". See the class documentation for details.
     * @param {Boolean} [cfg.sortNodes=true] When true, will sort the children of each node. For a "storeys" hierarchy, the
     * ````IfcBuildingStorey```` nodes will be ordered spatially, from the highest storey down to the lowest, on the
     * vertical World axis. For all hierarchy types, other node types will be ordered in the ascending alphanumeric order of their titles.
     * @param {Boolean} [cfg.pruneEmptyNodes=true] When true, will not contain nodes that don't have content in the {@link Scene}. These are nodes whose {@link MetaObject}s don't have {@link Entity}s.
     * @param {RenderService} [cfg.renderService] Optional {@link RenderService} to use. Defaults to the {@link TreeViewPlugin}'s default {@link RenderService}.
     */
    constructor(viewer, cfg = {}) {

        super("TreeViewPlugin", viewer);

        /**
         * Contains messages for any errors found while last rebuilding this TreeView.
         * @type {String[]}
         */
        this.errors = [];

        /**
         * True if errors were found generating this TreeView.
         * @type {boolean}
         */
        this.valid = true;

        const containerElement = cfg.containerElement || document.getElementById(cfg.containerElementId);

        if (!(containerElement instanceof HTMLElement)) {
            this.error("Mandatory config expected: valid containerElementId or containerElement");
            return;
        }

        for (let i = 0; ; i++) {
            if (!treeViews[i]) {
                treeViews[i] = this;
                this._index = i;
                this._id = `tree-${i}`;
                break;
            }
        }


        this._containerElement = containerElement;
        this._metaModels = {};
        this._autoAddModels = (cfg.autoAddModels !== false);
        this._autoExpandDepth = (cfg.autoExpandDepth || 0);
        this._sortNodes = (cfg.sortNodes !== false);
        this._viewer = viewer;
        this._rootElement = null;
        this._muteSceneEvents = false;
        this._muteTreeEvents = false;
        this._rootNodes = [];
        this._objectNodes = {}; // Object ID -> Node
        this._nodeNodes = {}; // Node ID -> Node
        this._rootNames = {}; // Node ID -> Root name
        this._sortNodes = cfg.sortNodes;
        this._pruneEmptyNodes = cfg.pruneEmptyNodes;
        this._showListItemElementId = null;
        this._renderService = cfg.renderService || new RenderService();

        if (!this._renderService) {
            throw new Error('TreeViewPlugin: no render service set');
        }

        this._containerElement.oncontextmenu = (e) => {
            e.preventDefault();
        };

        this._onObjectVisibility = this._viewer.scene.on("objectVisibility", (entity) => {
            if (this._muteSceneEvents) {
                return;
            }
            const objectId = entity.id;
            const node = this._objectNodes[objectId];
            if (!node) {
                return; // Not in this tree
            }
            const visible = entity.visible;
            const updated = (visible !== node.checked);
            if (!updated) {
                return;
            }
            this._muteTreeEvents = true;
            node.checked = visible;
            if (visible) {
                node.numVisibleEntities++;
            } else {
                node.numVisibleEntities--;
            }

            this._renderService.setCheckbox(node.nodeId, visible);

            let parent = node.parent;
            while (parent) {
                parent.checked = visible;
                if (visible) {
                    parent.numVisibleEntities++;
                } else {
                    parent.numVisibleEntities--;
                }

                this._renderService.setCheckbox(parent.nodeId, (parent.numVisibleEntities > 0));

                parent = parent.parent;
            }

            this._muteTreeEvents = false;
        });

        this._onObjectXrayed = this._viewer.scene.on('objectXRayed', (entity) => {
            if (this._muteSceneEvents) {
                return;
            }
            const objectId = entity.id;
            const node = this._objectNodes[objectId];
            if (!node) {
                return; // Not in this tree
            }
            this._muteTreeEvents = true;
            const xrayed = entity.xrayed;
            const updated = (xrayed !== node.xrayed);
            if (!updated) {
                return;
            }
            node.xrayed = xrayed;

            this._renderService.setXRayed(node.nodeId, xrayed);
            this._muteTreeEvents = false;
        });

        this._switchExpandHandler = (event) => {
            event.preventDefault();
            event.stopPropagation();
            const switchElement = event.target;
            this._expandSwitchElement(switchElement);
        };

        this._switchCollapseHandler = (event) => {
            event.preventDefault();
            event.stopPropagation();
            const switchElement = event.target;
            this._collapseSwitchElement(switchElement);
        };

        this._checkboxChangeHandler = (event) => {
            if (this._muteTreeEvents) {
                return;
            }
            this._muteSceneEvents = true;
            const checkbox = event.target;
            const visible = this._renderService.isChecked(checkbox);
            const nodeId = this._renderService.getIdFromCheckbox(checkbox);

            const checkedNode = this._nodeNodes[nodeId];
            const objects = this._viewer.scene.objects;
            let numUpdated = 0;
            
            this._withNodeTree(checkedNode, (node) => {
                const objectId = node.objectId;
                const entity = objects[objectId];
                const isLeaf = (node.children.length === 0);
                node.numVisibleEntities = visible ? node.numEntities : 0;
                if (isLeaf && (visible !== node.checked)) {
                    numUpdated++;
                }
                node.checked = visible;

                this._renderService.setCheckbox(node.nodeId, visible);
                
                if (entity) {
                    entity.visible = visible;
                }
            });

            let parent = checkedNode.parent;
            while (parent) {
                parent.checked = visible;
                
                if (visible) {
                    parent.numVisibleEntities += numUpdated;
                } else {
                    parent.numVisibleEntities -= numUpdated;
                }

                this._renderService.setCheckbox(parent.nodeId, (parent.numVisibleEntities > 0));
                
                parent = parent.parent;
            }
            this._muteSceneEvents = false;
        };

        this._hierarchy = cfg.hierarchy || "containment";
        this._autoExpandDepth = cfg.autoExpandDepth || 0;

        if (this._autoAddModels) {
            const modelIds = Object.keys(this.viewer.metaScene.metaModels);
            for (let i = 0, len = modelIds.length; i < len; i++) {
                const modelId = modelIds[i];
                const metaModel = this.viewer.metaScene.metaModels[modelId];
                if (metaModel.finalized) {
                    this.addModel(modelId);
                }
            }
            this.viewer.scene.on("modelLoaded", (modelId) => {
                if (this.viewer.metaScene.metaModels[modelId]) {
                    this.addModel(modelId);
                }
            });
        }

        this.hierarchy = cfg.hierarchy;
    }

    /**
     * Sets how the nodes are organized within this tree view.
     *
     * Accepted values are:
     *
     * * "containment" - organizes the nodes to indicate the containment hierarchy of the IFC objects.
     * * "types" - groups the nodes within their IFC types.
     * * "storeys" - groups the nodes within ````IfcBuildingStoreys```` and sub-groups them by their IFC types.
     *
     * <br>
     * This can be updated dynamically.
     *
     * Default value is "containment".
     *
     * @type {String}
     */
    set hierarchy(hierarchy) {
        hierarchy = hierarchy || "containment";
        if (hierarchy !== "containment" && hierarchy !== "storeys" && hierarchy !== "types") {
            this.error("Unsupported value for `hierarchy' - defaulting to 'containment'");
            hierarchy = "containment";
        }
        if (this._hierarchy === hierarchy) {
            return;
        }
        this._hierarchy = hierarchy;
        this._createNodes();
    }

    /**
     * Gets how the nodes are organized within this tree view.
     *
     * @type {String}
     */
    get hierarchy() {
        return this._hierarchy;
    }

    /**
     * Adds a model to this tree view.
     *
     * The model will be automatically removed when destroyed.
     *
     * To automatically add each model as it's created, instead of manually calling this method each time,
     * provide a ````autoAddModels: true```` to the TreeViewPlugin constructor.
     *
     * @param {String} modelId ID of a model {@link Entity} in {@link Scene#models}.
     * @param {Object} [options] Options for model in the tree view.
     * @param {String} [options.rootName] Optional display name for the root node. Ordinary, for "containment"
     * and "storeys" hierarchy types, the tree would derive the root node name from the model's "IfcProject" element
     * name. This option allows to override that name when it is not suitable as a display name.
     */
    addModel(modelId, options = {}) {
        if (!this._containerElement) {
            return;
        }
        const model = this.viewer.scene.models[modelId];
        if (!model) {
            throw "Model not found: " + modelId;
        }
        const metaModel = this.viewer.metaScene.metaModels[modelId];
        if (!metaModel) {
            this.error("MetaModel not found: " + modelId);
            return;
        }
        if (this._metaModels[modelId]) {
            this.warn("Model already added: " + modelId);
            return;
        }
        this._metaModels[modelId] = metaModel;

        if (options && options.rootName) {
            this._rootNames[modelId] = options.rootName;
        }
        
        model.on("destroyed", () => {
            this.removeModel(model.id);
        });
        this._createNodes();
    }

    /**
     * Removes a model from this tree view.
     *
     * Does nothing if model not currently in tree view.
     *
     * @param {String} modelId ID of a model {@link Entity} in {@link Scene#models}.
     */
    removeModel(modelId) {
        if (!this._containerElement) {
            return;
        }
        const metaModel = this._metaModels[modelId];
        if (!metaModel) {
            return;
        }

        if (this._rootNames[modelId]) {
            delete this._rootNames[modelId];
        }

        delete this._metaModels[modelId];
        this._createNodes();
    }

    /**
     * Highlights the tree view node that represents the given object {@link Entity}.
     *
     * This causes the tree view to collapse, then expand to reveal the node, then highlight the node.
     *
     * If a node is previously highlighted, de-highlights that node and collapses the tree first.
     *
     * Note that if the TreeViewPlugin was configured with ````pruneEmptyNodes: true```` (default configuration), then the
     * node won't exist in the tree if it has no Entitys in the {@link Scene}. in that case, nothing will happen.
     *
     * Within the DOM, the node is represented by an ````<li>```` element. This method will add a ````.highlighted-node```` class to
     * the element to make it appear highlighted, removing that class when de-highlighting it again. See the CSS rules
     * in the TreeViewPlugin examples for an example of that class.
     *
     * @param {String} objectId ID of the {@link Entity}.
     */
    showNode(objectId) {
        this.unShowNode();

        const node = this._objectNodes[objectId];
        if (!node) {
            return; // Node may not exist for the given object if (this._pruneEmptyNodes == true)
        }

        const nodeId = node.nodeId;

        const switchElement = this._renderService.getSwitchElement(nodeId);
        if (switchElement) {
            this._expandSwitchElement(switchElement);
            switchElement.scrollIntoView();
            return true;
        }
        
        const path = [];
        path.unshift(node);
        let parent = node.parent;
        while (parent) {
            path.unshift(parent);
            parent = parent.parent;
        }

        for (let i = 0, len = path.length; i < len; i++) {
            const switchElement = this._renderService.getSwitchElement(path[i].nodeId);
            if (switchElement) {
                this._expandSwitchElement(switchElement);
            }
        }

        this._renderService.setHighlighted(nodeId, true);

        this._showListItemElementId = nodeId;
    }

    /**
     * De-highlights the node previously shown with {@link TreeViewPlugin#showNode}.
     *
     * Does nothing if no node is currently shown.
     *
     * If the node is currently scrolled into view, keeps the node in view.
     */
    unShowNode() {
        if (!this._showListItemElementId) {
            return;
        }

        this._renderService.setHighlighted(this._showListItemElementId, false)
        
        this._showListItemElementId = null;
    }

    /**
     * Expands the tree to the given depth.
     *
     * Collapses the tree first.
     *
     * @param {Number} depth Depth to expand to.
     */
    expandToDepth(depth) {
        this.collapse();
        const expand = (node, countDepth) => {
            if (countDepth === depth) {
                return;
            }

            const switchElement = this._renderService.getSwitchElement(node.nodeId);
            if (switchElement) {
                this._expandSwitchElement(switchElement);
                const childNodes = node.children;
                for (var i = 0, len = childNodes.length; i < len; i++) {
                    const childNode = childNodes[i];
                    expand(childNode, countDepth + 1);
                }
            }
        };
        for (let i = 0, len = this._rootNodes.length; i < len; i++) {
            const rootNode = this._rootNodes[i];
            expand(rootNode, 0);
        }
    }

    /**
     * Closes all the nodes in the tree.
     */
    collapse() {
        for (let i = 0, len = this._rootNodes.length; i < len; i++) {
            const rootNode = this._rootNodes[i];
            const objectId = rootNode.objectId;
            this._collapseNode(objectId);
        }
    }

    /**
     * Iterates over a subtree of the tree view's {@link TreeViewNode}s, calling the given callback for each
     * node in depth-first pre-order.
     *
     * @param {TreeViewNode} node Root of the subtree.
     * @param {Function} callback Callback called at each {@link TreeViewNode}, with the TreeViewNode given as the argument.
     */
    withNodeTree(node, callback) {
        callback(node);
        const children = node.children;
        if (!children) {
            return;
        }
        for (let i = 0, len = children.length; i < len; i++) {
            this.withNodeTree(children[i], callback);
        }
    }

    /**
     * Destroys this TreeViewPlugin.
     */
    destroy() {
        if (!this._containerElement) {
            return;
        }
        this._metaModels = {};
        if (this._rootElement && !this._destroyed) {
            this._rootElement.parentNode.removeChild(this._rootElement);
            this._viewer.scene.off(this._onObjectVisibility);
            this._destroyed = true;
        }
        delete treeViews[this._index];
        super.destroy();
    }

    _createNodes() {
        if (this._rootElement) {
            this._rootElement.parentNode.removeChild(this._rootElement);
            this._rootElement = null;
        }

        this._rootNodes = [];
        this._objectNodes = {};
        this._nodeNodes = {};
        this._validate();
        if (this.valid || (this._hierarchy !== "storeys")) {
            this._createEnabledNodes();
        } else {
            this._createDisabledNodes();
        }
    }

    _validate() {
        this.errors = [];
        switch (this._hierarchy) {
            case "storeys":
                this.valid = this._validateMetaModelForStoreysHierarchy();
                break;
            case "types":
                this.valid = (this._rootNodes.length > 0);
                break;
            case "containment":
            default:
                this.valid = (this._rootNodes.length > 0);
                break;
        }
        return this.valid;
    }

    _validateMetaModelForStoreysHierarchy(level = 0, ctx, buildingNode) {
        // ctx = ctx || {
        //     foundIFCBuildingStoreys: false
        // };
        // const metaObjectType = metaObject.type;
        // const children = metaObject.children;
        // if (metaObjectType === "IfcBuilding") {
        //     buildingNode = true;
        // } else if (metaObjectType === "IfcBuildingStorey") {
        //     if (!buildingNode) {
        //         errors.push("Can't build storeys hierarchy: IfcBuildingStorey found without parent IfcBuilding");
        //         return false;
        //     }
        //     ctx.foundIFCBuildingStoreys = true;
        // }
        // if (children) {
        //     for (let i = 0, len = children.length; i < len; i++) {
        //         const childMetaObject = children[i];
        //         if (!this._validateMetaModelForStoreysHierarchy(childMetaObject, errors, level + 1, ctx, buildingNode)) {
        //             return false;
        //         }
        //     }
        // }
        // if (level === 0) {
        //     if (!ctx.foundIFCBuildingStoreys) {
        //         // errors.push("Can't build storeys hierarchy: no IfcBuildingStoreys found");
        //     }
        // }
        return true;
    }

    _createEnabledNodes() {
        if (this._pruneEmptyNodes) {
            this._findEmptyNodes();
        }
        switch (this._hierarchy) {
            case "storeys":
                this._createStoreysNodes();
                if (this._rootNodes.length === 0) {
                    this.error("Failed to build storeys hierarchy");
                }
                break;
            case "types":
                this._createTypesNodes();
                break;
            case "containment":
            default:
                this._createContainmentNodes();
        }
        if (this._sortNodes) {
            this._doSortNodes();
        }
        this._synchNodesToEntities();
        this._createTrees();
        this.expandToDepth(this._autoExpandDepth);
    }

    _createDisabledNodes() {

        const rootNode = this._renderService.createRootNode();
        this._rootElement = rootNode;
        this._containerElement.appendChild(rootNode);

        const rootMetaObjects = this._viewer.metaScene.rootMetaObjects;

        for (let objectId in rootMetaObjects) {
            const rootMetaObject = rootMetaObjects[objectId];
            const metaObjectType = rootMetaObject.type;
            const metaObjectName = rootMetaObject.name;
            const rootName = ((metaObjectName && metaObjectName !== ""
                && metaObjectName !== "Undefined"
                && metaObjectName !== "Default") ? metaObjectName : metaObjectType);

            const childNode = this._renderService.createDisabledNodeElement(rootName);
            rootNode.appendChild(childNode);
        }
    }


    _findEmptyNodes() {
        const rootMetaObjects = this._viewer.metaScene.rootMetaObjects;
        for (let objectId in rootMetaObjects) {
            this._findEmptyNodes2(rootMetaObjects[objectId]);
        }
    }

    _findEmptyNodes2(metaObject, countEntities = 0) {
        const viewer = this.viewer;
        const scene = viewer.scene;
        const children = metaObject.children;
        const objectId = metaObject.id;
        const entity = scene.objects[objectId];
        metaObject._countEntities = 0;
        if (entity) {
            metaObject._countEntities++;
        }
        if (children) {
            for (let i = 0, len = children.length; i < len; i++) {
                const childMetaObject = children[i];
                childMetaObject._countEntities = this._findEmptyNodes2(childMetaObject);
                metaObject._countEntities += childMetaObject._countEntities;
            }
        }
        return metaObject._countEntities;
    }

    _createStoreysNodes() {
        const rootMetaObjects = this._viewer.metaScene.rootMetaObjects;
        for (let id in rootMetaObjects) {
            this._createStoreysNodes2(rootMetaObjects[id], null, null, null);
        }
    }

    _createStoreysNodes2(metaObject, buildingNode, storeyNode, typeNodes) {
        if (this._pruneEmptyNodes && (metaObject._countEntities === 0)) {
            return;
        }
        const metaObjectType = metaObject.type;
        const metaObjectName = metaObject.name;
        const children = metaObject.children;
        const objectId = metaObject.id;
        if (metaObjectType === "IfcBuilding") {
            buildingNode = {
                nodeId: `${this._id}-${objectId}`,
                objectId: objectId,
                title: (metaObject.metaModels.length === 0) ? "na" : this._rootNames[metaObject.metaModels[0].id] || ((metaObjectName && metaObjectName !== "" && metaObjectName !== "Undefined" && metaObjectName !== "Default") ? metaObjectName : metaObjectType),
                type: metaObjectType,
                parent: null,
                numEntities: 0,
                numVisibleEntities: 0,
                checked: false,
                xrayed: false,
                children: []
            };
            this._rootNodes.push(buildingNode);
            this._objectNodes[buildingNode.objectId] = buildingNode;
            this._nodeNodes[buildingNode.nodeId] = buildingNode;
        } else if (metaObjectType === "IfcBuildingStorey") {
            if (!buildingNode) {
                this.error("Failed to build storeys hierarchy for model '" + this.metaModel.id + "' - model does not have an IfcBuilding object, or is not an IFC model");
                return;
            }
            storeyNode = {
                nodeId: `${this._id}-${objectId}`,
                objectId: objectId,
                title: (metaObjectName && metaObjectName !== "" && metaObjectName !== "Undefined" && metaObjectName !== "Default") ? metaObjectName : metaObjectType,
                type: metaObjectType,
                parent: buildingNode,
                numEntities: 0,
                numVisibleEntities: 0,
                checked: false,
                xrayed: false,
                children: []
            };
            buildingNode.children.push(storeyNode);
            this._objectNodes[storeyNode.objectId] = storeyNode;
            this._nodeNodes[storeyNode.nodeId] = storeyNode;
            typeNodes = {};
        } else {
            if (storeyNode) {
                const objects = this._viewer.scene.objects;
                const object = objects[objectId];
                if (object) {
                    typeNodes = typeNodes || {};
                    let typeNode = typeNodes[metaObjectType];
                    if (!typeNode) {
                        typeNode = {
                            nodeId: `${this._id}-${storeyNode.objectId}-${metaObjectType}`,
                            objectId: `${storeyNode.objectId}-${metaObjectType}`,
                            title: metaObjectType,
                            type: metaObjectType,
                            parent: storeyNode,
                            numEntities: 0,
                            numVisibleEntities: 0,
                            checked: false,
                            xrayed: false,
                            children: []
                        };
                        storeyNode.children.push(typeNode);
                        this._objectNodes[typeNode.objectId] = typeNode;
                        this._nodeNodes[typeNode.nodeId] = typeNode;
                        typeNodes[metaObjectType] = typeNode;
                    }
                    const node = {
                        nodeId: `${this._id}-${objectId}`,
                        objectId: objectId,
                        title: (metaObjectName && metaObjectName !== "" && metaObjectName !== "Undefined" && metaObjectName !== "Default") ? metaObjectName : metaObjectType,
                        type: metaObjectType,
                        parent: typeNode,
                        numEntities: 0,
                        numVisibleEntities: 0,
                        checked: false,
                        xrayed: false,
                        children: []
                    };
                    typeNode.children.push(node);
                    this._objectNodes[node.objectId] = node;
                    this._nodeNodes[node.nodeId] = node;
                }
            }
        }
        if (children) {
            for (let i = 0, len = children.length; i < len; i++) {
                const childMetaObject = children[i];
                this._createStoreysNodes2(childMetaObject, buildingNode, storeyNode, typeNodes);
            }
        }
    }

    _createTypesNodes() {
        const rootMetaObjects = this._viewer.metaScene.rootMetaObjects;
        for (let id in rootMetaObjects) {
            this._createTypesNodes2(rootMetaObjects[id], null, null);
        }
    }

    _createTypesNodes2(metaObject, rootNode, typeNodes) {
        if (this._pruneEmptyNodes && (metaObject._countEntities === 0)) {
            return;
        }
        const metaObjectType = metaObject.type;
        const metaObjectName = metaObject.name;
        const children = metaObject.children;
        const objectId = metaObject.id;
        if (!metaObject.parent) {
            rootNode = {
                nodeId: `${this._id}-${objectId}`,
                objectId: objectId,
                title: metaObject.metaModels.length === 0 ? "na" : this._rootNames[metaObject.metaModels[0].id] || ((metaObjectName && metaObjectName !== "" && metaObjectName !== "Undefined" && metaObjectName !== "Default") ? metaObjectName : metaObjectType),
                type: metaObjectType,
                parent: null,
                numEntities: 0,
                numVisibleEntities: 0,
                checked: false,
                xrayed: false,
                children: []
            };
            this._rootNodes.push(rootNode);
            this._objectNodes[rootNode.objectId] = rootNode;
            this._nodeNodes[rootNode.nodeId] = rootNode;
            typeNodes = {};
        } else {
            if (rootNode) {
                const objects = this._viewer.scene.objects;
                const object = objects[objectId];
                if (object) {
                    let typeNode = typeNodes[metaObjectType];
                    if (!typeNode) {
                        typeNode = {
                            nodeId: `${this._id}-${rootNode.objectId}-${metaObjectType}`,
                            objectId: `${rootNode.objectId}-${metaObjectType}`,
                            title: metaObjectType,
                            type: metaObjectType,
                            parent: rootNode,
                            numEntities: 0,
                            numVisibleEntities: 0,
                            checked: false,
                            xrayed: false,
                            children: []
                        };
                        rootNode.children.push(typeNode);
                        this._objectNodes[typeNode.objectId] = typeNode;
                        this._nodeNodes[typeNode.nodeId] = typeNode;
                        typeNodes[metaObjectType] = typeNode;
                    }
                    const node = {
                        nodeId: `${this._id}-${objectId}`,
                        objectId: objectId,
                        title: (metaObjectName && metaObjectName !== "" && metaObjectName !== "Default") ? metaObjectName : metaObjectType,
                        type: metaObjectType,
                        parent: typeNode,
                        numEntities: 0,
                        numVisibleEntities: 0,
                        checked: false,
                        xrayed: false,
                        children: []
                    };
                    typeNode.children.push(node);
                    this._objectNodes[node.objectId] = node;
                    this._nodeNodes[node.nodeId] = node;
                }
            }
        }
        if (children) {
            for (let i = 0, len = children.length; i < len; i++) {
                const childMetaObject = children[i];
                this._createTypesNodes2(childMetaObject, rootNode, typeNodes);
            }
        }
    }

    _createContainmentNodes() {
        const rootMetaObjects = this._viewer.metaScene.rootMetaObjects;
        for (let id in rootMetaObjects) {
            this._createContainmentNodes2(rootMetaObjects[id], null);
        }
    }

    _createContainmentNodes2(metaObject, parent) {
        if (this._pruneEmptyNodes && (metaObject._countEntities === 0)) {
            return;
        }
        const metaObjectType = metaObject.type;
        const metaObjectName = metaObject.name || metaObjectType;
        const children = metaObject.children;
        const objectId = metaObject.id;
        const node = {
            nodeId: `${this._id}-${objectId}`,
            objectId: objectId,
            title: (!parent) ? metaObject.metaModels.length === 0 ? "na" : (this._rootNames[metaObject.metaModels[0].id] || metaObjectName) : (metaObjectName && metaObjectName !== "" && metaObjectName !== "Undefined" && metaObjectName !== "Default") ? metaObjectName : metaObjectType,
            type: metaObjectType,
            parent: parent,
            numEntities: 0,
            numVisibleEntities: 0,
            checked: false,
            xrayed: false,
            children: []
        };
        if (parent) {
            parent.children.push(node);
        } else {
            this._rootNodes.push(node);
        }
        this._objectNodes[node.objectId] = node;
        this._nodeNodes[node.nodeId] = node;
        if (children) {
            for (let i = 0, len = children.length; i < len; i++) {
                const childMetaObject = children[i];
                this._createContainmentNodes2(childMetaObject, node);
            }
        }
    }

    _doSortNodes() {
        for (let i = 0, len = this._rootNodes.length; i < len; i++) {
            const rootNode = this._rootNodes[i];
            this._sortChildren(rootNode);
        }
    }

    _sortChildren(node) {
        const children = node.children;
        if (!children || children.length === 0) {
            return;
        }
        if (this._hierarchy === "storeys" && node.type === "IfcBuilding") {
            // Assumes that children of an IfcBuilding will always be IfcBuildingStoreys
            children.sort(this._getSpatialSortFunc());
        } else {
            children.sort(this._alphaSortFunc);
        }
        for (let i = 0, len = children.length; i < len; i++) {
            const node = children[i];
            this._sortChildren(node);
        }
    }

    _getSpatialSortFunc() { // Creates cached sort func with Viewer in scope
        const viewer = this.viewer;
        const scene = viewer.scene;
        const camera = scene.camera;
        const metaScene = viewer.metaScene;
        return this._spatialSortFunc || (this._spatialSortFunc = (node1, node2) => {
            if (!node1.aabb || !node2.aabb) {
                // Sorting on lowest point of the AABB is likely more more robust when objects could overlap storeys
                if (!node1.aabb) {
                    node1.aabb = scene.getAABB(metaScene.getObjectIDsInSubtree(node1.objectId));
                }
                if (!node2.aabb) {
                    node2.aabb = scene.getAABB(metaScene.getObjectIDsInSubtree(node2.objectId));
                }
            }
            let idx = 0;
            if (camera.xUp) {
                idx = 0;
            } else if (camera.yUp) {
                idx = 1;
            } else {
                idx = 2;
            }
            if (node1.aabb[idx] > node2.aabb[idx]) {
                return -1;
            }
            if (node1.aabb[idx] < node2.aabb[idx]) {
                return 1;
            }
            return 0;
        });
    }

    _alphaSortFunc(node1, node2) {
        const title1 = node1.title.toUpperCase(); // FIXME: Should be case sensitive?
        const title2 = node2.title.toUpperCase();
        if (title1 < title2) {
            return -1;
        }
        if (title1 > title2) {
            return 1;
        }
        return 0;
    }

    _synchNodesToEntities() {
        const objectIds = Object.keys(this.viewer.metaScene.metaObjects);
        const metaObjects = this._viewer.metaScene.metaObjects;
        const objects = this._viewer.scene.objects;
        for (let i = 0, len = objectIds.length; i < len; i++) {
            const objectId = objectIds[i];
            const metaObject = metaObjects[objectId];
            if (metaObject) {
                const node = this._objectNodes[objectId];
                if (node) {
                    const entity = objects[objectId];
                    if (entity) {
                        const visible = entity.visible;
                        node.numEntities = 1;
                        node.xrayed = entity.xrayed;
                        if (visible) {
                            node.numVisibleEntities = 1;
                            node.checked = true;
                        } else {
                            node.numVisibleEntities = 0;
                            node.checked = false;
                        }
                        let parent = node.parent; // Synch parents
                        while (parent) {
                            parent.numEntities++;
                            if (visible) {
                                parent.numVisibleEntities++;
                                parent.checked = true;
                            }
                            parent = parent.parent;
                        }
                    }
                }
            }
        }
    }

    _withNodeTree(node, callback) {
        callback(node);
        const children = node.children;
        if (!children) {
            return;
        }
        for (let i = 0, len = children.length; i < len; i++) {
            this._withNodeTree(children[i], callback);
        }
    }

    _createTrees() {
        if (this._rootNodes.length === 0) {
            return;
        }
        const rootNodeElements = this._rootNodes.map((rootNode) => {
            return this._createNodeElement(rootNode);
        });

        const rootNode = this._renderService.createRootNode();
        rootNodeElements.forEach((nodeElement) => {
            rootNode.appendChild(nodeElement);
        });

        this._containerElement.appendChild(rootNode);
        this._rootElement = rootNode;
    }

    _createNodeElement(node) {
        const contextmenuHandler = (event) =>{
                this.fire("contextmenu", {
                event: event,
                viewer: this._viewer,
                treeViewPlugin: this,
                treeViewNode: node
            });
            event.preventDefault();
        };
        const onclickHandler = (event) => {
            this.fire("nodeTitleClicked", {
                event: event,
                viewer: this._viewer,
                treeViewPlugin: this,
                treeViewNode: node
            });
            event.preventDefault();
        };

        return this._renderService.createNodeElement(node, this._switchExpandHandler, this._checkboxChangeHandler, contextmenuHandler, onclickHandler);
    }

    _expandSwitchElement(switchElement) {
        const expanded = this._renderService.isExpanded(switchElement); 
        if (expanded) {
            return;
        }

        const nodeId = this._renderService.getId(switchElement);

        const nodeElements = this._nodeNodes[nodeId].children.map((node) => {
            return this._createNodeElement(node);
        });

        this._renderService.addChildren(switchElement, nodeElements)

        this._renderService.expand(switchElement, this._switchExpandHandler, this._switchCollapseHandler);
    }

    _collapseNode(nodeId) {
        const switchElement = this._renderService.getSwitchElement(nodeId);
        this._collapseSwitchElement(switchElement);
    }

    _collapseSwitchElement(switchElement) {
        this._renderService.collapse(switchElement, this._switchExpandHandler, this._switchCollapseHandler);
    }
}