Creating the display filter

Summary

In this chapter, we will create the DisplayFilter object, which will help use our application data to render any nodes matching a filtering criterion.

Concepts

  • Gathering UI filter selections

  • Rendering the scene with your filtered data

  • Adding additional rendering options


Creating a helper class

Although we have linked our application data to our viewer in a simple sense, all we are really doing is displaying this associated data. Let’s build upon the idea of using this data to further the experience of our application. We want to be able to visualize the data we are linking in an effective, straightforward manner. While looking through a large list of numbers of stocks and prices to sort what values are of concern can work, it’s much easier to visually represent this information.

We will use the idea of a filter to gather nodes that meet particular criterion set by the user. In our template, we have two double-ended sliders for setting a price range and an inventory range. We also have push buttons indicating particular manufacturers for particular parts. This will be the filter criterion.

We will start by creating a new file called DisplayFilter.js and placing it alongside our app.js file in the src subdirectory. The constructor will take in a WebViewer object. Because we will make this entire module available to our main class, we will add the default keyword to the class declaration.

export default class DisplayFilter {
        constructor(viewer) {
                this._viewer = viewer;
        }
}

Using class properties to track values

In order to limit the amount of calls to the DOM to obtain slider values, we can recognize that only one slider will be changing its value at a time. So, let’s create a Map that stores each slider value and updates the single value as it changes. We can then reference the values in the Map, rather than making four slower calls to the DOM each time a UI callback is fired.

constructor(viewer) {
        this._viewer = viewer;
        this._sliderVals = new Map();
        let sliderElements = document.querySelectorAll("#psMinSlider, #psMaxSlider, #ssMinSlider, #ssMaxSlider");
        sliderElements.forEach((x) => {
                let slider = x;
                switch (slider.id) {
                        case "psMinSlider":
                                this._sliderVals.set("psMinVal", slider.value);
                                break;
                        case "psMaxSlider":
                                this._sliderVals.set("psMaxVal", slider.value);
                                break;
                        case "ssMinSlider":
                                this._sliderVals.set("ssMinVal", slider.value);
                                break;
                        case "ssMaxSlider":
                                this._sliderVals.set("ssMaxVal", slider.value);
                                break;
                        default:
                                console.log("Slider element value not found.");
                }
        });
}

Upon creation of this filter, we should also gather what company selections have been made from the UI buttons on our page. If the button is depressed, the company is enabled, otherwise, it is disabled. We will keep track of enabled companies with a class member variable. Within your constructor, gather the company names. Note that the alt tag for the images are identical to the company name in the database information.

Using filter values

We now have an initial collection of filter criterion, so let’s set up a function to actually filter our nodes based on these choices.

We will write a member function of the DisplayFilter class called gatherFilteredNodes. This public function will take in the _modelData map we created in the main application. Using this model data, we will loop through the data and determine which nodes fall within the filter criterion. We will do this for price, stock, and company, then aggregate the data and extract the like nodes between them, for our final filtered result.

gatherFilteredNodes(modelData) {
        let pMinVal = parseInt(this._sliderVals.get("psMinVal"));
        let pMaxVal = parseInt(this._sliderVals.get("psMaxVal"));
        let sMinVal = parseInt(this._sliderVals.get("ssMinVal"));
        let sMaxVal = parseInt(this._sliderVals.get("ssMaxVal"));
        let pNodesPassed = [];
        let sNodesPassed = [];
        let mNodesPassed = [];
        let valuesIterator = modelData.values();
        for (let nodeValues of valuesIterator) {
                if (nodeValues.Price >= pMinVal && nodeValues.Price <= pMaxVal) {
                        pNodesPassed.push(nodeValues.ID);
                }
                if (nodeValues.Stock >= sMinVal && nodeValues.Stock <= sMaxVal) {
                        sNodesPassed.push(nodeValues.ID);
                }
                if (this._companyFilter.indexOf(nodeValues.Manufacturer) !== -1) {
                        mNodesPassed.push(nodeValues.ID);
                }
        }
        let psNodesPassed = pNodesPassed.filter(value => -1 !== sNodesPassed.indexOf(value));
        this._filteredNodes = psNodesPassed.filter(value => -1 !== mNodesPassed.indexOf(value));
}

You can see we are adding nodes to an array parameter called _filteredNodes, so let’s be sure to initialize this member in our constructor.

this._filteredNodes = [];

Reset the scene

We now have the functionality to gather nodes based on the filter options and model data, but we need to use this to update the rendering of these nodes now. We will implement several rendering techniques to display this data. To start, we will simply show nodes that meet the filter criterion and hide nodes that do not.

Start by writing another member function called setRenderingSelection(). This function will reset the scene as needed, then call the appropriate API functions to render the filtered nodes. Depending on the complexity of the model, these operations can take some extra time. As a result, a few of these APIs return promises, so in order to properly sequence our calls, we will wait for all the promises to resolve before continuing to our next call.

To control the visibility of selected nodes, we have an API call named setNodesVisibility(). Because we are only tracking the filtered nodes, and are unaware of the other node IDs not within the filter, it’s easy to quickly hide the entire model, then subsequently set the visibility to show the filtered nodes. Our operations are fast enough to where the user cannot distinguish these actions, but do note that for very large models, the two distinct actions could be discernable depending on the amount of work your application is doing. In that case, it would be better to also track the “unfiltered nodes” and only hide those provided nodes (rather than hiding the whole model each time).

While we are only focusing on hiding and showing nodes right now, we will add other rendering techniques that may require other reset-like functions at the model level, much like we are resetting the visibility of the model each time. For that reason, we will preemptively set up a promise array for each called API that returns a promise. We will then use Promise.all() to ensure all promises are resolved, before calling the visibility function again to show the filtered nodes.

setRenderingSelection() {
        let promiseArray = [];
        promiseArray.push(this._viewer.model.setNodesVisibility([this._viewer.model.getAbsoluteRootNode()], true));

        Promise.all(promiseArray)
                .then(() => {
                        this._viewer.model.setNodesVisibility([this._viewer.model.getAbsoluteRootNode()], false)
                                .then(() => this._viewer.model.setNodesVisibility(this._filteredNodes, true));

        });
}

This is all we would need to hide and show nodes, but let’s add a few other rendering techniques you could perform. We can see from the template that there are options to view in a transparent mode, and to view in an “X-Ray” mode. Let’s add each of these options to our rendering function.

First, we need a way to obtain what the filter selection is. We could query this each time in the rendering function, but since we are calling this function many times, we don’t want to have to slow down our performance with calls to the DOM. Therefore, let’s track it in a class variable, and only update the selection when it changes.

Start by declaring a member variable _filterSelection in the DisplayFilter constructor. Since our default choice is “isolate”, we can initialize it with that option.

this._filterSelection = "isolateChoice";

At the same time, we will make a setter function on the DisplayFilter class for later use.

setFilterSelection(filter) {
        this._filterSelection = filter;
}

If we look at the HTML template, we can see the IDs for the other options are “transparentChoice” and “xrayChoice”. We can use these IDs to set conditionals in our rendering function. In the opacity option, we want to reset the opacities for the model before filtering. We can add this to the promise array we set up earlier. We can then call setNodesOpacity() in the same way we called setNodesVisibility(). We will make the filtered nodes opaque and the non-filtered nodes more transparent.

let promiseArray = [];
        promiseArray.push(this._viewer.model.setNodesVisibility([this._viewer.model.getAbsoluteRootNode()], true));
        promiseArray.push(this._viewer.model.setNodesOpacity([this._viewer.model.getAbsoluteRootNode()], 1.0));
        Promise.all(promiseArray)
                .then(() => {
                if (this._filterSelection === "isolateChoice") {
                        this._viewer.model.setNodesVisibility([this._viewer.model.getAbsoluteRootNode()], false)
                                .then(() => this._viewer.model.setNodesVisibility(this._filteredNodes, true));
                }
                else if (this._filterSelection === "transparentChoice") {
                        this._viewer.model.setNodesOpacity([this._viewer.model.getAbsoluteRootNode()], 0.25);
                        this._viewer.model.setNodesOpacity(this._filteredNodes, 1.0);
                }
        });

The last rendering option, “X-Ray Mode”, behaves somewhat differently than setting the visibility or opacity. X-Ray mode is enabled by setting the whole render mode for the viewer, not just how particular nodes are rendered. In X-Ray mode, selected items are rendered opaque and other items outside the selection set are rendered with a unique transparency. To reset the default rendering each time the UI is interacted with, we have to reset the selections and the render mode.

setRenderingSelection() : void {

        if (this._viewer.view.getDrawMode() === Communicator.DrawMode.XRay) {
                this._viewer.selectionManager.clear();
                this._viewer.view.setDrawMode(Communicator.DrawMode.WireframeOnShaded);
        }

This resets the scene (like we were doing when resetting visibilities and transparencies). To enable the x-ray rendering, we set the draw mode to x-ray, and then manually add each nodeId to the selection set to make the selection opaque.

else if (this._filterSelection === "xrayChoice") {
        this._viewer.view.setDrawMode(Communicator.DrawMode.XRay);
        let sm = this._viewer.selectionManager;

        sm.clear();
        this._filteredNodes.forEach(nodeId => {
                sm.selectNode(nodeId, Communicator.SelectionMode.Add);
        });
}

We now have the pieces for the functionality of our filter elements, but now we must actually hook up the UI elements to this functionality. Once that’s done, we’ll be able to change the rendering mode on our model, as seen here: