16 Jun 2021

Add Data Visualization Heatmaps for Rooms of non-Revit model part I - NWC

 

Last year, my colleague Xiaodong blogged about AEC Model Data. It's additional, AEC-specific metadata in the JSON format with information about the BIM model extracted by the Model Derivative service that contains building levels, construction phases, viewports, etc.

But Model Derivative service will only generate AEC Model Data for Revit models. We cannot get similar data for other BIM model formats like IFC, NWC (included NWD), and so on currently, unfortunately. Therefore, we cannot make the Data Visualization extension's Heatmap work properly within non-Revit models. The Data Visualization extension takes advantage of the AEC Model Data to generate the level-room map for heatmaps by default.

Aec Model Data

 

However, with the newly-released Forge Viewer v7.45, we can rebuild a similar building-level data structure to Revit's AEC Model Data, rebuild the level-room map, then make the heatmap work for the NWC/NWD model with the following tricks.

 (Note. These tricks are working only for NWC exported from Revit. More specifically, the NWC must be exported from an RVT model. This approach also works for the NWD published from NWC exported from Revit. i.e. Open the NWC of Revit and publish it as NWD in Navisworks.)

 

First, let's start rebuilding levels.

1st step of rebuilding levels: We use Viewer3D#search to get building level info from Revit properties.

async function findLevels(model) {
    return new Promise((resolve, reject) => {
        model.search(
            'Layer',
            ( dbIds ) => {
                const levels = [];
                const tree = model.getInstanceTree();

                for( let i=0; i<dbIds.length; i++ ) {
                    const dbId = dbIds[i];
                    const name = tree.getNodeName( dbId );

                    if( name.includes( '<No level>' ) ) continue;

                    levels.push({
                        guid: dbId,
                        name,
                        dbId,
                        extension: {
                            buildingStory: true,
                            structure: false,
                            computationHeight: 0,
                            groundPlane: false,
                            hasAssociatedViewPlans: false,
                        }
                    });
                }
                resolve(levels);
            },
            reject,
            ['Icon']
        );
    });
}

const levels = await findLevels(viewer.model);

2nd step of rebuilding levels: Rebuild a similar building-level data structure to Revit's AEC Model Data by using the new function: Autodesk.AEC.AecModelData.computeLevels. Before using it, don't forget to load Autodesk.AEC.LevelsExtension extension. 

const levelExt = await viewer.loadExtension('Autodesk.AEC.LevelsExtension');

levelExt.setAecModelData(undefined, viewer.model); //!<<< Clear before reset

const aecdata = Autodesk.AEC.AecModelData.computeLevels(levels, viewer.model); //!<<< Rebuild aec model data
levelExt.setAecModelData(aecdata, viewer.model); //!<<< assign new aec model data to levels extension
levelExt._updateOccluderData();

Note. With the computeLevels function, we can only obtain an AEC Model Data that contains levels and refPointTransformation data like this.

Aec Model Data

 

Second, we will rebuild room data for the Data Visualization extension. Here is the code snippet I used to achieve this. Before starting this step, don't forget to load Autodesk.DataVisualization extension.

const dataVizExtn = await viewer.loadExtension('Autodesk.DataVisualization');

async function getBoxAsync(dbId, model) {
    return new Promise((resolve, reject) => {
        const tree = model.getInstanceTree();
        const frags = model.getFragmentList();
        tree.enumNodeFragments(dbId, function(fragid) {
            let bounds = new THREE.Box3();
            frags.getWorldBounds(fragid, bounds);
            return resolve(bounds);
        }, true);
    });
}

function getLevel(box, floors) {
    const currentElevation = box.min.z;

    if (currentElevation < floors[0].zMin) {
        return floors[0];
    } else if (currentElevation > floors[floors.length - 1].zMax) {
        return floors[floors.length - 1];
    } else {
        return floors.find(f => f.zMin <= currentElevation && f.zMax >= currentElevation );
    }
}

async function getPropAsync(dbId, model) {
    return new Promise((resolve, reject) => {
        model.getProperties(dbId, result => resolve(result));
    });
}

function getRoomName(dbId, model) {
    const tree = model.getInstanceTree();
    return tree.getNodeName( dbId );
}

async function getRoomDbIds(model) {
    return new Promise((resolve, reject) => {
        model.search( 'Rooms', resolve, reject, ['Category'])
    });
}

async function buildRoomMap(viewer) {
    const model = viewer.model;
    const levelExt = await viewer.loadExtension('Autodesk.AEC.LevelsExtension');
    const floors = levelExt.floorSelector.floorData;

    const dbIds = await getRoomDbIds(model);
    const DataVizCore = Autodesk.DataVisualization.Core;
    let levelRoomsMap = new DataVizCore.LevelRoomsMap();

    for ( let i = dbIds.length - 1; i >= 0; i-- ) {
        const dbId = dbIds[i];
        const box = await getBoxAsync(dbId, model);
        const level = getLevel(box, floors);
        const name =  getRoomName(dbId, model);

        let room = new DataVizCore.Room(
            dbId, //Room's DbId
            name,
            box
        );

        levelRoomsMap.addRoomToLevel(level.name, room);
    }

    return levelRoomsMap;
}

const levelRoomsMap = await buildRoomMap(viewer.model, viewer);

After executing the above code snippet, we will get the level-room map like the below:

Level-Room Map

 

Third, let's mock some sensors to see what the heatmap will like

const DataVizCore = Autodesk.DataVisualization.Core;
const devices = [];

for(let lvl in levelRoomsMap) {
    const rooms = levelRoomsMap[lvl];
    for (let i = rooms.length - 1; i >= 0; i--) {
        const room = rooms[i];
        const center = room.bounds.center();
        const device = {
            id: `${room.name} Device`, // An ID to identify this device
            position: center, // World coordinates of this device
            sensorTypes: ['temperature'], // The types/properties this device exposes
        }

        devices.push(device);
        room.addDevice(device);
    }
}

const structureInfo = new DataVizCore.ModelStructureInfo(viewer.model);
let shadingData = await structureInfo.generateSurfaceShadingData(devices, levelRoomsMap);

await dataVizExtn.setupSurfaceShading(viewer.model, shadingData);

// Set heatmap colors for temperature
dataVizExtn.registerSurfaceShadingColors('temperature', [0x00ff00, 0xff0000]);

function getSensorValue() {
    return Math.random(); // Your random function 🙂
}

// Generate the heatmap graphics (do this one time, this is heavier)
dataVizExtn.renderSurfaceShading(Object.keys(levelRoomsMap), 'temperature', getSensorValue);
// Do this as many times as you want (this is lightweight)
dataVizExtn.updateSurfaceShading(getSensorValue);

viewer.getExtension('Autodesk.AEC.LevelsExtension').floorSelector.selectFloor(0);

Here is the final result:

Heatmap on NWC

Lastly, put them together:

async function findLevels(model) {
    return new Promise((resolve, reject) => {
        model.search(
            'Layer',
            ( dbIds ) => {
                const levels = [];
                const tree = model.getInstanceTree();

                for( let i=0; i<dbIds.length; i++ ) {
                    const dbId = dbIds[i];
                    const name = tree.getNodeName( dbId );

                    if( name.includes( '<No level>' ) ) continue;

                    levels.push({
                        guid: dbId,
                        name,
                        dbId,
                        extension: {
                            buildingStory: true,
                            structure: false,
                            computationHeight: 0,
                            groundPlane: false,
                            hasAssociatedViewPlans: false,
                        }
                    });
                }
                resolve(levels);
            },
            reject,
            ['Icon']
        );
    });
}

async function computeAecModelDataForNwcOfRevit(viewer) {
    const model = viewer.model;
    const levelExt = await viewer.loadExtension('Autodesk.AEC.LevelsExtension');

    levelExt.setAecModelData(undefined, model); //!<<< Clear before reset

    const levels = await findLevels(model);
    const aecdata = Autodesk.AEC.AecModelData.computeLevels(levels, model); //!<<< Rebuild aec model data
    levelExt.setAecModelData(aecdata, model); //!<<< assign new aec model data to levels extension
    levelExt._updateOccluderData();
}

async function getBoxAsync(dbId, model) {
    return new Promise((resolve, reject) => {
        const tree = model.getInstanceTree();
        const frags = model.getFragmentList();
        tree.enumNodeFragments(dbId, function(fragid) {
            let bounds = new THREE.Box3();
            frags.getWorldBounds(fragid, bounds);
            return resolve(bounds);
        }, true);
    });
}

function getLevel(box, floors) {
    const currentElevation = box.min.z;

    if (currentElevation < floors[0].zMin) {
        return floors[0];
    } else if (currentElevation > floors[floors.length - 1].zMax) {
        return floors[floors.length - 1];
    } else {
        return floors.find(f => f.zMin <= currentElevation && f.zMax >= currentElevation );
    }
}

async function getPropAsync(dbId, model) {
    return new Promise((resolve, reject) => {
        model.getProperties(dbId, result => resolve(result));
    });
}

function getRoomName(dbId, model) {
    const tree = model.getInstanceTree();
    return tree.getNodeName( dbId );
}

async function getRoomDbIds(model) {
    return new Promise((resolve, reject) => {
        model.search( 'Rooms', resolve, reject, ['Category'])
    });
}

async function buildRoomMap(viewer) {
    const model = viewer.model;
    const levelExt = await viewer.loadExtension('Autodesk.AEC.LevelsExtension');
    const floors = levelExt.floorSelector.floorData;

    const dbIds = await getRoomDbIds(model);
    const DataVizCore = Autodesk.DataVisualization.Core;
    let levelRoomsMap = new DataVizCore.LevelRoomsMap();

    for ( let i = dbIds.length - 1; i >= 0; i-- ) {
        const dbId = dbIds[i];
        const box = await getBoxAsync(dbId, model);
        const level = getLevel(box, floors);
        const name =  getRoomName(dbId, model);

        let room = new DataVizCore.Room(
            dbId, //Room's DbId
            name,
            box
        );

        levelRoomsMap.addRoomToLevel(level.name, room);
    }

    return levelRoomsMap;
}


async function initHeatmap(viewer) {
    const model = viewer.model;
    const dataVizExtn = await viewer.loadExtension('Autodesk.DataVisualization');
    const DataVizCore = Autodesk.DataVisualization.Core;
    const levelRoomsMap = await buildRoomMap(viewer);
    const devices = [];

    for(let lvl in levelRoomsMap) {
        const rooms = levelRoomsMap[lvl];
        for (let i = rooms.length - 1; i >= 0; i--) {
            const room = rooms[i];
            const center = room.bounds.center();
            const device = {
                id: `${room.name} Device`, // An ID to identify this device
                position: center, // World coordinates of this device
                sensorTypes: ['temperature'], // The types/properties this device exposes
            }

            devices.push(device);
            room.addDevice(device);
        }
    }

    const structureInfo = new DataVizCore.ModelStructureInfo(model);
    let shadingData = await structureInfo.generateSurfaceShadingData(devices, levelRoomsMap);

    await dataVizExtn.setupSurfaceShading(model, shadingData);

    // Set heatmap colors for temperature
    dataVizExtn.registerSurfaceShadingColors('temperature', [0x00ff00, 0xff0000]);

    function getSensorValue() {
        return Math.random(); // Your random function 🙂
    }

    // Generate the heatmap graphics (do this one time, this is heavier)
    dataVizExtn.renderSurfaceShading(Object.keys(levelRoomsMap), 'temperature', getSensorValue);
    // Do this as many times as you want (this is lightweight)
    dataVizExtn.updateSurfaceShading(getSensorValue);

    viewer.getExtension('Autodesk.AEC.LevelsExtension').floorSelector.selectFloor(0);
}


await computeAecModelDataForNwcOfRevit(viewer);
await initHeatmap(viewer);

That's it! Hope you enjoy it~ 

Related Article