Accessing metadata and IFC properties

In the last section, we loaded an IFC model. This is sufficient if all you want to do is view the 3D model in a limited way. However, many BIM models contain useful information concerning both IFC data types and associated properties. These types and properties allow you to interact with the model more intelligently, both in what is seen visually, and in being able to extract useful information for downstream applications.

In this section, we’re going to learn how to extract metadata from the imported CAD file. We’ll begin with a simple example to extract some metadata attached to the top of the component hierachy - the ModelFile object - and later develop this into a more general sample.Note that for this tutorial, we’ll use the terms “metadata” and “properties” interchangeably.

All objects derived from the HPS::Component class can have attached metadata, which is represented by a key-value pair. In this example, we are going to request metadata with the key “units” attached to the ModelFile object. Open IFCUtils.cpp, and find the following the code:

UTF8 IFCUtils::GetUnitsString(HPS::CADModel cadModel)
{
	if (cadModel.GetComponentType() != HPS::Component::ComponentType::ExchangeModelFile)
		return "";

	HPS::StringMetadata units_metadata = cadModel.GetMetadata("Units");
	if (units_metadata.Empty())
		return "mm";

	HPS::UTF8 full = units_metadata.GetValue();
	if (full == "Millimeter")
		return "mm";
	else if (full == "Centimeter")
		return "cm";
	else if (full == "Meter")
		return "m";
	else if (full == "Kilometer")
		return "km";
	else if (full == "Inch")
		return "in";
	else if (full == "Foot")
		return "ft";
	else if (full == "Yard")
		return "yd";
	else if (full == "Mile")
		return "mi";
	else
	{
		// Just use the full name
		return full;
	}
}

Note how the value of the metadata for “units”, is assigned to a string object of type HPS::UTF8, which is the basic string data type used by HOOPS Visualize. Metadata can have different value types. and we’ll handle the different types later.

Let’s call this function from the Sandbox. For simplicity, we can use one of the User Code slots that are defined in CHPSView. Scroll down to the OnUserCode1 function, and you’ll see this:

void CHPSView::OnUserCode1()
{
        HPS::View  view = _canvas.GetFrontView();
        HPS::Model model = view.GetAttachedModel();
        HPS::SegmentKey modelKey = model.GetSegmentKey();
        HPS::CADModel cadModel = GetCHPSDoc()->GetCADModel();
        if (!cadModel.Empty())
        {
                UTF8 unitsString = IFCUtils::GetUnitsString(cadModel);
                HPS::UTF8  utf8Name = cadModel.GetName();
                CString cName(utf8Name.GetBytes());
                MessageBox(CString("Units: ") + unitsString.GetBytes(), CString("Model File:") + cName);

                std::vector < std::pair<UTF8, UTF8>>  wsMetaData;
                IFCUtils::GetComponentMetaData(cadModel, wsMetaData);
                UTF8 allProps;
                for (auto const &wsd : wsMetaData)
                {
                        allProps += wsd.first + ":" + wsd.second + "\n";
                }
                MessageBox(CString(allProps.GetBytes()), CString("Model File") + cName);
        }
}

If you run the Sandbox, then click the User Code 1 button:

../../_images/OnUserCode1.png

…the code above will run, and produce these dialogs with unit and properties data read from the model:

../../_images/msgbox_units.png ../../_images/msgbox_properties.png

Notice that we are iterating through the component metadata to read the properties of this file. We’ve also added the name of the file to the title of the dialog. Like other properties, the file name is just metadata, but since asking for the name of a component is such a common operation, the convenience function GetName() is provided, which can be called directly.

For this example, we are printing the units in a modal dialog, but you can obviously use whatever method is appropriate for your situation.

Finally, we’re going to look at obtaining all the metadata associated with a a component. We do this using the function HPS::Component::GetAllMetaData. The units and name properties explained above were retrieved as string property values, but metadata can have several value types. The function below demonstrates how to handle the different value types. In this case, we’re going to just translate them to type HPS::UTF8 for display in the user interface:

UTF8 IFCUtils::GetMetadataValueAsUTF8(HPS::Metadata const & metadata)
{
	UTF8  rValue;
	std::stringstream ss;

	switch (metadata.Type())
	{
	case HPS::Type::StringMetadata:
		ss << HPS::StringMetadata(metadata).GetValue();
		rValue = HPS::StringMetadata(metadata).GetValue();
		break;
	case HPS::Type::UnsignedIntegerMetadata:
		ss << HPS::UnsignedIntegerMetadata(metadata).GetValue();
		break;
	case HPS::Type::IntegerMetadata:
		ss << HPS::IntegerMetadata(metadata).GetValue();
		break;
	case HPS::Type::DoubleMetadata:
		ss << HPS::DoubleMetadata(metadata).GetValue();
		break;
	case HPS::Type::TimeMetadata:
		ss << HPS::TimeMetadata(metadata).GetValue();
		break;
	case HPS::Type::BooleanMetadata:
		ss << HPS::BooleanMetadata(metadata).GetValue();
		break;
	default:
		ASSERT(0);	//Are we missing a metadata type?	
	}

	rValue = UTF8(ss.str().c_str());
	return rValue;
}

You can then assemble all the metadata values into a std::vector of pair values is easy using the GetComponentMetaData function:

void IFCUtils::GetComponentMetaData(HPS::Component component, std::vector< std::pair<UTF8, UTF8>>  &properties)
{
	HPS::MetadataArray mdArray = component.GetAllMetadata();

	for (auto const &md : mdArray)
	{
		UTF8 utf8Value = GetMetadataValueAsUTF8(md);

		std::pair<UTF8, UTF8> pair(md.GetName(), utf8Value);
		properties.push_back(pair);
	}
}

Isolating IFC storeys

Next, we’re going to look at both handling IFC property values in particular and making use of them in the user interface. In particular, we will use the IFCBUILDINGSTOREY property attached to our model to allow the user to view different levels of the model.

The examples in this section assume familiarity with adding controls to an MFC based application, but the same principles apply to any GUI you may be using. We’ll begin by adding a new panel to the User Code table of the Ribbon Bar. We will use this panel to add additional controls later on. Having created an IFC panel, now add a Combox box to it as shown. Change its caption to Floors and its appearance to Drop List. Also for readability, let’s change the ID name to something more appropriate. In this case, ID_COMBO_FLOOR.

../../_images/floors_combo.png

Having created the control, the next thing we want to do is populate it when we load an IFC file. Let’s add a function to the the CView object to do this. In some cases you might consider associating IFC document information with the MFC document object, but for the sake of simplicity in this tutorial, we’ll keep in in the CView. Add a new function to the CView called OnLoadedIFCFile(). We will call this function from the CDocument::OnOpenDocument function when we load a new IFC file.

HPS::Exchange::ImportOptionsKit options;

success = ImportExchangeFile(lpszPathName, options);

if (success)
{
	CHPSView * cview = GetCHPSView();
	cview->UpdatePlanes();

	if (_cadModel.Empty() && _defaultCamera.Empty())
		cview->GetCanvas().GetFrontView().ComputeFitWorldCamera(_defaultCamera);

	if (!_stricmp(ext, "ifc"))
		cview->OnLoadedIFCFile();
}

Once the document is opened, we can search for components which represent a building story, then use this information to populate a dropdown list.


	 HPS::Component::ComponentType cType = cadModel.GetComponentType();
	 ComponentArray      ancestorcomponents;

	 ifcFindIFCStoreys(cadModel, ancestorcomponents, gblStoreyComponents);

	 //
	 // now populate the dropdown
	 //
	 CHPSFrame * pFrame = (CHPSFrame*)AfxGetMainWnd();
	 CMFCRibbonComboBox *pCombo = DYNAMIC_DOWNCAST(CMFCRibbonComboBox, pFrame->m_wndRibbonBar.FindByID(ID_COMBO_FLOOR));
	 ifcPopulateFloorsDropDown(pCombo, gblStoreyComponents);

In order to find the building stories, we will recursively walk down the component hierarchy - starting with the root CAD mode - until we find product occurrences with a property named “IFCBUILDINGSTOREY”.

Notice in the function below, we gather the component path of the selected product occurrence. The component path represents a unique, ordered set of components from a leaf node back up to the root, in this case a CAD model. More on HPS::ComponentPath can be found here.

The reason we are using component paths and not just the component is to allow use of Show, Hide, and Isolate functions all of which operate on paths. Just like the KeyPath entity of HOOPS Visualize can be used to define a unique instance of a graphics entity, so the component path defines a unique component instance. In this specific case, we know that an IFCBUILDINGSTOREY must be reprented by a product occurrence component, so we only need to search components of this type.


void  ifcFindIFCStoreys(HPS::Component component, ComponentArray &ancestorcomponents, ComponentPathArray &componentPaths)
{
	//
	// Get the Exchange Component Type
	// the IFCStory type will always be represented as Product Occurrence. If it not one of those or 
	// the root model file, then do not search any deeper
	//
	HPS::Component::ComponentType cType = component.GetComponentType();

	if (cType != HPS::Component::ComponentType::ExchangeProductOccurrence && cType != HPS::Component::ComponentType::ExchangeModelFile)
		return;

	ancestorcomponents.push_back(component);

	if (cType == HPS::Component::ComponentType::ExchangeProductOccurrence)
	{
		UTF8 strType;
		if (GetMetaDataType(component, strType))
		{

			if (strType == "IFCBUILDINGSTOREY")
			{
				for (auto cmp : ancestorcomponents)
				{
					HPS::UTF8 strName = cmp.GetName();
					CString cstrName(strName.GetBytes());
				}

				//  Component Path needs to have components ordered leaf -> root
				//  For convenience we build it up the ancestor list vector root->leaf
				//   Create a reversed vector
				ComponentArray reverseComponents(ancestorcomponents);
				std::reverse(reverseComponents.begin(), reverseComponents.end());
				componentPaths.push_back(ComponentPath(reverseComponents));
				ancestorcomponents.pop_back();

				return;
			}
		}
	}

	HPS::ComponentArray carray = component.GetSubcomponents();
	for (auto comp : carray)
	{
		ifcFindIFCStories(comp, ancestorcomponents, componentPaths);
	}

	ancestorcomponents.pop_back();

}

Having built up an array of component paths, we will use this to populate the dropdown list. The first entry in the list will be used to show all of the building. The other entries will represent one of the building storeys we found previously.

void ifcPopulateFloorsDropDown(CMFCRibbonComboBox *pCombo, IFCSampleModel *pIFCModel)
{
        // The first entry will be to show the entire model
        pCombo->AddItem(CString("All"),NULL);
        pCombo->SelectItem(0);

        // now add an entry for each
        int index = 0;
        for (auto path : pIFCModel->_storeyPaths)
        {
                //lets get the name of the component
                float elevation = pIFCModel->_storeyElevations[index];

                Component front = path.Front();

                HPS::UTF8 strFrontName = front.GetName();
                CString csName(strFrontName.GetBytes());

                CString csValue;
                csValue.Format(_T("- %f"),  elevation);

                pCombo->AddItem(csName + csValue, index++);
        }
}

Finally, lets add an event handler for when the dropdown selection is changed. We call the Isolate function on the component path representing a floor. For the ‘All’ selection, we can call HPS::CADModel:: ResetVisibility.

 void CHPSView::OnComboFloor()
 {
	 CHPSFrame * pFrame = (CHPSFrame*)AfxGetMainWnd();
	 CMFCRibbonComboBox *pCombo = DYNAMIC_DOWNCAST(CMFCRibbonComboBox, pFrame->m_wndRibbonBar.FindByID(ID_COMBO_FLOOR));

	 int selectionIndex = pCombo->GetCurSel();
	 if (selectionIndex == 0)
	 {
		 HPS::CADModel cadModel = GetCHPSDoc()->GetCADModel();
		 cadModel.ResetVisibility(_canvas);
	 }
	 else
	 {
		 // show the selected floor
		 int pathIndex = selectionIndex - 1;
		 gblStoreyComponents[pathIndex].Isolate(_canvas);
	 }
	 _canvas.Update();
 }

This is what the model looks like after isloating the foundations:

../../_images/ifcmodel_foundations.png