MEP System Structure in Hierarchical JSON Graph
Originally Published inThe Building Coder
Yesterday, I presented the new TraverseAllSystems ↗ add-in to traverse all MEP system graphs and export their connected hierarchical structure to JSON and XML that I am helping the USC ↗ team with here at the San Francisco cloud accelerator.
I continued with that today, and also integrated a minor enhancement to RevitLookup:
- TraverseAllSystems updates
- Shared parameter creation
- Options
- Bottom-up JSON structure
- Top-down JSON structure
- TraversalTree JSON output generator
- TreeNode JSON output generator
- Download and to do
- RevitLookup updates
TraverseAllSystems Updates
The aim of the TraverseAllSystems project is to present the MEP system graphs in a separate tree view panel integrated in the Forge viewer ↗ and hook up the tree view nodes bi-directionally with the 2D and 3D viewer elements.
To achieve that, I implemented a couple of significant enhancements over the simple XML file storage:
- Store the MEP system graph structure in JSON instead of XML
- Implement both bottom-up and top-down storage according to the jsTree JSON spec ↗.
- Support both element id and UniqueId node identifiers.
- Store the JSON output in a shared parameter attached to the MEP system element, so that it is automatically included in the Forge SVF translation generated from the RVT input file.
Here is a list of the update releases so far:
- 2017.0.0.2 ↗ – implemented visited element dictionary to prevent infinite recursion loop
- 2017.0.0.3 ↗ – implemented DumpToJson
- 2017.0.0.4 ↗ – implemented shared parameter creation
- 2017.0.0.5 ↗ – implemented shared parameter value population, tested and verified graph structure json is written out
- 2017.0.0.6 ↗ – renamed json text field to name
- 2017.0.0.7 ↗ – implemented top-down json graph storage
- 2017.0.0.8 ↗ – automatically create shared parameter, eliminated separate command, wrap json strings in double quotes, validated json output
Shared Parameter Creation
I implemented a new SharedParameterMgr
class to create the shared parameter to store the JSON output in.
This class is based on the ExportCncFab ↗ ExportParameters.cs module ↗.
The shared parameter is automatically created if not already present, as in the following usage example:
// Check for shared parameter
// to store graph information.
Definition def = SharedParameterMgr.GetDefinition(
desirableSystems.First<MEPSystem>() );
if( null == def )
{
SharedParameterMgr.Create( doc );
def = SharedParameterMgr.GetDefinition(
desirableSystems.First<MEPSystem>() );
if( null == def )
{
message = "Error creating the "
+ "storage shared parameter.";
return Result.Failed;
}
}
Here is the SharedParameterMgr
class implementation:
/// <summary>
/// Shared parameters to keep store MEP system
/// graph structure in JSON strings.
/// </summary>
class SharedParameterMgr
{
/// <summary>
/// Define the user visible shared parameter name.
/// </summary>
const string _shared_param_name = "MepSystemGraphJson";
/// <summary>
/// Return the parameter definition from
/// the given element and parameter name.
/// </summary>
public static Definition GetDefinition( Element e )
{
IList<Parameter> ps = e.GetParameters(
_shared_param_name );
int n = ps.Count;
Debug.Assert( 1 >= n,
"expected maximum one shared parameters "
+ "named " + _shared_param_name );
Definition d = ( 0 == n )
? null
: ps[0].Definition;
return d;
}
/// <summary>
/// Create a new shared parameter definition
/// in the specified grpup.
/// </summary>
static Definition CreateNewDefinition(
DefinitionGroup group,
string parameter_name,
ParameterType parameter_type )
{
return group.Definitions.Create(
new ExternalDefinitionCreationOptions(
parameter_name, parameter_type ) );
}
/// <summary>
/// Create the shared parameter.
/// </summary>
public static void Create( Document doc )
{
/// <summary>
/// Shared parameters filename; used only in case
/// none is set and we need to create the export
/// history shared parameters.
/// </summary>
const string _shared_parameters_filename
= "shared_parameters.txt";
const string _definition_group_name
= "TraverseAllSystems";
Application app = doc.Application;
// Retrieve shared parameter file name
string sharedParamsFileName
= app.SharedParametersFilename;
if( null == sharedParamsFileName
|| 0 == sharedParamsFileName.Length )
{
string path = Path.GetTempPath();
path = Path.Combine( path,
_shared_parameters_filename );
StreamWriter stream;
stream = new StreamWriter( path );
stream.Close();
app.SharedParametersFilename = path;
sharedParamsFileName
= app.SharedParametersFilename;
}
// Retrieve shared parameter file object
DefinitionFile f
= app.OpenSharedParameterFile();
using( Transaction t = new Transaction( doc ) )
{
t.Start( "Create TraverseAllSystems "
+ "Shared Parameters" );
// Create the category set for binding
CategorySet catSet = app.Create.NewCategorySet();
Category cat = doc.Settings.Categories.get_Item(
BuiltInCategory.OST_DuctSystem );
catSet.Insert( cat );
cat = doc.Settings.Categories.get_Item(
BuiltInCategory.OST_PipingSystem );
catSet.Insert( cat );
Binding binding = app.Create.NewInstanceBinding(
catSet );
// Retrieve or create shared parameter group
DefinitionGroup group
= f.Groups.get_Item( _definition_group_name )
?? f.Groups.Create( _definition_group_name );
// Retrieve or create the three parameters;
// we could check if they are already bound,
// but it looks like Insert will just ignore
// them in that case.
Definition definition
= group.Definitions.get_Item( _shared_param_name )
?? CreateNewDefinition( group,
_shared_param_name, ParameterType.Text );
doc.ParameterBindings.Insert( definition, binding,
BuiltInParameterGroup.PG_GENERAL );
t.Commit();
}
}
}
Options
I implemented a new Options
class to control two settings:
- Use element id or UniqueId for to identify node
- Store JSON graph bottom-up or top-down
The class implementation is short, sweet and trivial:
class Options
{
/// <summary>
/// Store element id or UniqueId in JSON output?
/// </summary>
public static bool StoreUniqueId = false;
public static bool StoreElementId = !StoreUniqueId;
/// <summary>
/// Store parent node id in child, or recursive
/// tree of children in parent?
/// </summary>
public static bool StoreJsonGraphBottomUp = false;
public static bool StoreJsonGraphTopDown
= !StoreJsonGraphBottomUp;
}
The two bottom-up and top-down JSON storage structures both comply with the jsTree JSON spec ↗.
Bottom-Up JSON Structure
[
{ "id" : "ajson1", "parent" : "#", "text" : "Simple root node" },
{ "id" : "ajson2", "parent" : "#", "text" : "Root node 2" },
{ "id" : "ajson3", "parent" : "ajson2", "text" : "Child 1" },
{ "id" : "ajson4", "parent" : "ajson2", "text" : "Child 2" },
]
Top-Down JSON Structure
{
id: -1,
name: 'Root',
children: [
{
id: 0,
name: 'Mechanical System',
children: [
{
id: 0_1,
name: 'Child 0_1',
type: 'window',
otherField: 'something...',
children: [
{
id: 0_1_1,
name: 'Grandchild 0_1_1'
}]
}, {
id: 0_2,
name: 'Child 0_2',
children: [
{
id: 0_2_1,
name: 'Grandchild 0_2_1'
}]
}]
}, {
id: 2,
name: 'Electrical System',
children: [
{
id: 2_1,
name: 'Child 2_1',
children: [{
id: 2_1_1,
name: 'Grandchild 2_1_1'
}]
},
{
id: 2_2,
name: 'Child 2_2',
children: [{
id: 2_2_1,
name: 'Grandchild 2_2_1'
}]
}]
},
{
id: 3,
name: 'Piping System',
children: [
{
id: 3_1,
name: 'Child 3_1',
children: [{
id: 3_1_1,
name: 'Grandchild 3_1_1'
}]
},
{
id: 3_2,
name: 'Child 3_2',
children: [{
id: 3_2_1,
name: 'Grandchild 3_2_1'
}]
}]
}]
}
TraversalTree JSON Output Generator
The two TraversalTree
JSON output generators DumpToJsonTopDown
and DumpToJsonBottomUp
are pretty trivial as well, since all the work is done by the individual tree nodes:
/// <summary>
/// Dump the top-down traversal graph into JSON.
/// In this case, each parent node is populated
/// with a full hierarchical graph of all its
/// children, cf. https://www.jstree.com/docs/json.
/// </summary>
public string DumpToJsonTopDown()
{
return m_startingElementNode
.DumpToJsonTopDown();
}
/// <summary>
/// Dump the bottom-up traversal graph into JSON.
/// In this case, each child node is equipped with
/// a 'parent' pointer, cf.
/// https://www.jstree.com/docs/json/
/// </summary>
public string DumpToJsonBottomUp()
{
List<string> a = new List<string>();
m_startingElementNode.DumpToJsonBottomUp( a, "#" );
return "[" + string.Join( ",", a ) + "]";
}
TreeNode JSON Output Generator
The two TreeNode
JSON output generators are only slightly more complicated.
Here are the two formatting strings that they use:
/// <summary>
/// Format a tree node to JSON storing parent id
/// in child node for bottom-up structure.
/// </summary>
const string _json_format_to_store_parent_in_child
= "{{"
+ "\"id\" : {0}, "
+ "\"name\" : \"{1}\", "
+ "\"parent\" : {2}}}";
/// <summary>
/// Format a tree node to JSON storing a
/// hierarchical tree of children ids in parent
/// for top-down structure.
/// </summary>
const string _json_format_to_store_children_in_parent
= "{{"
+ "\"id\" : {0}, "
+ "\"name\" : \"{1}\", "
+ "\"children\" : [{2}]}}";
Here are the two recursive functions implementing the JSON output:
static string GetName( Element e )
{
return e.Name.Replace( "\"", "'" );
}
static string GetId( Element e )
{
return Options.StoreUniqueId
? "\"" + e.UniqueId + "\""
: e.Id.IntegerValue.ToString();
}
/// <summary>
/// Add JSON strings representing all children
/// of this node to the given collection.
/// </summary>
public void DumpToJsonBottomUp(
List<string> json_collector,
string parent_id )
{
Element e = GetElementById( m_Id );
string id = GetId( e );
string json = string.Format(
_json_format_to_store_parent_in_child,
id, GetName( e ), parent_id );
json_collector.Add( json );
foreach( TreeNode node in m_childNodes )
{
node.DumpToJsonBottomUp( json_collector, id );
}
}
/// <summary>
/// Return a JSON string representing this node and
/// including the recursive hierarchical graph of
/// all its all children.
/// </summary>
public string DumpToJsonTopDown()
{
Element e = GetElementById( m_Id );
List<string> json_collector = new List<string>();
foreach( TreeNode child in m_childNodes )
{
json_collector.Add( child.DumpToJsonTopDown() );
}
string json_kids = string.Join( ",", json_collector );
string json = string.Format(
_json_format_to_store_children_in_parent,
GetId( e ), GetName( e ), json_kids );
return json;
}
Download and To Do
The current state of this project is available from the TraverseAllSystems GitHub repository ↗, and the version discussed above is release 2017.0.0.9 ↗.
The next step will consist of the Forge viewer extension implementation displaying a custom panel in the user interface hosting a tree view of the MEP system graphs and implementing two-way linking and selection functionality back and forth between the tree view nodes and the 2D and 3D viewer elements.
RevitLookup Updates
A couple of enhancement have been added to RevitLookup ↗ since I last mentioned it, most lately by awmcc90 ↗ and Shayne Hamel ↗ to handle exceptions snooping MEP elements, electrical circuits, flex ducts and flex pipes.
Here are the diffs:
- 2017.0.0.5 ↗ – merged pull request #14 by Shayneham to handle exceptions snooping flex pipe and duct lacking levels etc.
- 2017.0.0.4 ↗ – merged pull request #13 by awmcc90 to skip mepSys.Elements for OST_ElectricalInternalCircuits category
Thank you very much for those improvements!
If you run into any issues with RevitLookup yourself, please fork the repository, implement and test your changes, and issue a pull request for me to integrate them back into the master branch.
Thank you!