Brief code samples: More...

Brief code samples:

How to define your own relationships

Also see Full Working Example

{
...
};
extern "C" int MdlMain (int, char**)
{
static MyHandler s_handler; // Singleton
Relationship::GetManager().RegisterHandler (RELATIONID_MyRelationship, privateKeyPart, s_handler);
...
}
static void CreateMyRelationship ()
{
...
// In order to fill out an Relationship, we need a Relation.
// In order to create a Relation, we need a relationship element.
// So, that gives us the following bootstrapping sequence:
// 1. Create an element to represent the relationship itself.
// This element can be of any type, with or without graphics. In this demo,
// we choose to use a cell with a child shape to represent the relationship.
//
msElementDescr* relationshipElement = createRelationshipElement ();
// 2. Set up a relationship XAttribute to point to the subject and datum elements.
// Attach the linkage to the relationship element
//
// This ties this Relation to MyHandler
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
Relation rel (relationshipElement, RELATIONID_MyRelationship, privateKeyPart, true);
// ^^^^
// Give ownership of relationshipElement to rel
// 3. Add monikers to point to inputs and/or outputs
mdlDisplayPath_setCursorElemIndex (dp, mdlDisplayPath_getCount(dp)-1); // point to complex component
Moniker target (DgnPlatform::PersistentSnapPath (ACTIVEMODEL, (SnapPathCP)dp));
rel.AddMoniker (NULL, PID_MyTarget, target); // PID_MyTarget is an integer that I assigned to this moniker for my convenience
// Add other targets ...
// 4. Write the new relationship element to the model
rel.GetElemHandle().AddToModel (ACTIVEMODEL);
// Trigger the initial call to MyHandler::Reevaluate
}

Full Working Example

Shows how a simple Relation is defined, how its Relationship::Handler cooperates with IGraphEvaluation to build the dependency graph, and how its Handler reevaluates the relationship when called upon after the input is changed.

In this example, a dependent element is related to a datum element. The dependent is to be located at a fixed offset from the datum. When the datum element is changed, the relationship's handler is notified, and it moves the dependent element if necessary to maintain the fixed offset.

// Includes
// For MdlMain
// For Utilities
// For Relationship Handler
using namespace std;
// Defines
#define PID_Datum (ParameterId)0
#define PID_Dependent (ParameterId)1
// NB: These IDs are not real IDs. A relationship handler major ID must be a value allocated from the XAttributeHandlerMajorIDs value space. Edit msdefs.h to allocate an XAttributeHandlerMajorID.
#define RELATIONID_TestRel 1997
#define RELATIONID_Align_MinorId 0
// Example relationship data
{
DPoint3d m_point;
PointParamData (DPoint3d const& p) : m_point(p) {;}
PointParamData () {;}
virtual void Store (Bentley::MstnPlatform::Relationship::DataOutput& sink) const override {sink.put (&m_point.x, sizeof(m_point)/sizeof(m_point.x));}
virtual StatusInt Load (Bentley::MstnPlatform::Relationship::DataInput& source) override {source.get (&m_point.x, sizeof(m_point)/sizeof(m_point.x)); return SUCCESS;}
};
static DPoint3d computeOffset (Moniker&, Moniker&);
static void moveElement (Moniker&, Moniker&, DPoint3d const&);
extern MSElementDescrP createRelationshipElement ();
// Relationship Handler
{
virtual WString GetDescription (Relation const&) override {return L"Align Handler";}
virtual bool EnterGraph (IGraphEvaluation& g, Relation const& alignRel) override
{
ParameterQuery datum (alignRel, PID_Datum);
ParameterQuery dependent(alignRel, PID_Dependent);
if (!datum.IsTargetAvailable() || !dependent.IsTargetAvailable())
return false;
g.AddInput (alignRel, datum.GetMoniker().EvaluateElement().GetElementRef());
g.AddOutput (alignRel, dependent.GetMoniker().EvaluateElement().GetElementRef());
return false;
}
virtual void Reevaluate (IGraphEvaluation& graph, Relation& alignRel) override
{
ParameterQuery datum (alignRel, PID_Datum);
ParameterQuery dependent(alignRel, PID_Dependent);
PointParamData offsetData;
datum.GetData (offsetData); // Note how the datum parameter has both a moniker and data
moveElement (dependent.GetMoniker(), datum.GetMoniker(), offsetData.m_point);
}
}; // AlignHandler
static AlignHandler s_alignHandler;
// Create the relationship.
static void align_create (SnapPathP datumDP, SnapPathP dependentDP)
{
// Note that a moniker can capture a DgnPlatform::SnapPath. In fact, Moniker uses a DgnPlatform::PersistentSnapPath.
// That allows a moniker to store, not just an pointer to an element, but also a location
// on that element. In this example, the relationship will maintain a fixed offset between
// the elements measured from keypoints on each element. The SnapPaths identify the keypoints.
Moniker dependentMon (DgnPlatform::PersistentSnapPath (ACTIVEMODEL,dependentDP));
Moniker datumMoniker (DgnPlatform::PersistentSnapPath (ACTIVEMODEL,datumDP));
// Create a new element and schedule a Relation XAttribute to be added to it.
Relation rel (createRelationshipElement(), RELATIONID_TestRel, RELATIONID_Align_MinorId, true);
// Reference the datum element and the dependent element
rel.AddMoniker (NULL, PID_Datum, datumMoniker);
rel.AddMoniker (NULL, PID_Dependent, dependentMon);
// Note that the datum parameter will have both a moniker and data
DPoint3d offset = computeOffset (dependentMon, datumMoniker);
rel.AddParameterDataItem (NULL, PID_Datum, PointParamData(offset));
// Note that the relationship is stored in its own element in this example.
// You could store it on the dependent or on the datum element instead.
rel.GetElemHandle().AddToModel (ACTIVEMODEL);
}
// Main
extern "C" int MdlMain (int argc, char **argv)
{
...
Relationship::GetManager()->RegisterHandler (RELATIONID_TestRel, RELATIONID_Align_MinorId, &s_alignHandler);
return SUCCESS;
}
// Helper functions
static DPoint3d computeOffset (Moniker& dependent, Moniker& datum)
{
DPoint3d locDependent;
dependent.EvaluatePoint (locDependent);
DPoint3d locDatum;
datum.EvaluatePoint (locDatum);
DPoint3d offset;
bsiDPoint3d_subtractDPoint3dDPoint3d (&offset, &locDependent, &locDatum);
return offset;
}
static void moveElement (Moniker& dependent, Moniker& datum, DPoint3d const& offset)
{
DPoint3d currOffset = computeOffset (dependent, datum);
DPoint3d adjust;
bsiDPoint3d_subtractDPoint3dDPoint3d (&adjust, &offset, &currOffset);
if (bsiDPoint3d_magnitude (&adjust) < 1.0e-15)
return; // don't dirty the element if there's no real change
Transform trans;
bsiTransform_initIdentity (&trans);
bsiTransform_setTranslation (&trans, &adjust);
DgnPlatform::EditElementHandle ehDependent = dependent.EvaluateElement ();
ehDependent.GetHandler(MISSING_HANDLER_PERMISSION_Transform).ApplyTransform (ehDependent, DgnPlatform::TransformInfo(trans));
ehDependent.ReplaceInModel ();
}

Resolving Cycles

The first step in resolving cycles is to detect them by writing an event handler that:

Note that resolving a cycle in this way involves making a temporary change to the run-time graph. It does not require the persistent relationship elements to be modified. The temporary block relation can be created on the fly. In the following example, we call CreateTemporaryRelationshipElement to create the block relation.

In the following example, the logic for recognizing the cycle has been left out. It's up to you to know how to recognize the cycles that you can handle.

struct MyEventhandler : IGraphManagerEventHandler
{
...
virtual void OnCyclesDetected (IGraphEvaluation& graph) override
{
for (RefRefPairVectorVector::const_iterator i = graph.GetCycles().begin(); i != graph.GetCycles().end(); ++i)
{
T_RefRefPairVector const& aCycle = *i;
if (aCycle is one of mine) // ... somehow recognize cycles that you can repair ...
{
// Collect the relations in the cycle
ElementRefSet relsInCycle;
for (T_RefRefPairVector::const_iterator iCycleMember = aCycle.begin(); iCycleMember != aCycle.end(); ++iCycleMember)
relsInCycle.insert (iCycleMember->first);
// Create a new relation. This will be based on a temporary element ref, since it only pertains to
// this one evaluation of the graph.
DgnPlatform::EditElementHandle eh;
if (graph.CreateTemporaryRelationshipElement (eh, MYCYCLERESOLVERMAJORID, MYCYCLERESOLVERMINORID))
return;
Relation cycleResolver (eh.GetElementRef(), eh.GetModelRef());
// Subsitute the new relation for the whole list of relations in the cycle.
graph.RemoveRelationshipsAndSubstituteBlock (cycleResolver, relsInCycle);
graph.TopologyChanged (); // don't forget to call this!
return;
}
}
}

The next step is to write an IHandler for the relation that stands in for the cycle. This IHandler will probably use the relations that were replaced. Here is an example where the block relation just iterates over the original relations. That's just one way to resolve a cycle.

struct CycleResolver : public Bentley::Relationship::HandlerBase
{
virtual void Reevaluate (IGraphEvaluation& graph, Relation& rel) override
{
// Note that the graph remembers which relations were replaced by our cycleResolver relation.
ElementRefSet rels = graph.GetRelationshipsRemovedByBlockSubstitute (rel);
int niters = 0;
do {
// As an example, show how a cycle resolver could delegate to the original relations,
// assuming that it knows the order in which they should be called. In this example
// we don't actually know the order. We just call them in any order.
for (ElementRefSet::const_iterator i= rels.begin(); i != rels.end(); ++i)
{
Relation irel (*i, NULL);
g_rmanager->GetHandler(irel)->Reevaluate (graph, irel);
}
}
while (++niters < 10 && !IsSatisfied (&graph, rel));
// As an example, show how a cycle resolver might iterate to find a solution
}
...
};

Here is how to register the event handler and the block relationship handler in your main function:

void cycleResolver_main ()
{
static CycleResolver s_handler;
Relationship::GetManager().RegisterHandler (MYCYCLERESOLVERMAJORID, MYCYCLERESOLVERMINORID, &s_handler);
static CycleResolverEventhandler s_eventHandler;
}

Is element a member of your relation? Forward search.

ElementRefP anElement = ...
for (DependentElemRef dep = elementRef_getFirstDependent (anElement); NULL != dep; dep = elementDependent_getNext (dep))
{
Relation depRel (depRef, NULL);
if (!depRel.IsOpenOnElement ())
continue;
if (depRel.GetHandlerPublicIdPart() == RELATIONID_XYZ
&& depRel.GetHandlerPrivateIdPart() == YOUR_MINOR_ID)
{
// anElement is a member of your relation
}
}

Is element a member of your relation? Backward search.

Build the graph, starting from a given element:

GraphEvaluationPtr graph = mdlSystem_getMstnApp()->GetRelationshipManager().WhatIfChanged (anElement));

And then check to see what it's connected to:

graph->GetElementConnections (&inputTo, &outputFrom, anElement);
for (ElementRefVector::const_iterator i = inputTo.begin(); i != inputTo.end(); ++i)
{
Relation rel (*i);
if (rel.GetHandlerPublicIdPart() == RELATIONID_XYZ
&& rel.GetHandlerPrivateIdPart() == YOUR_MINOR_ID)
{
// anElement is a input to your relation
}
}
// similar loop over outputFrom to detect if anElement is an output of your relation

This is more expensive than the brute force method above and is pointless if you have a simple, explicit set of relationships. It is useful if you have a directed graph and/or if the structure of your graph is not known until your Relation's EnterGraph method is called. For directed graphs, WhatIfChanged does a topological sort.

Adding Monikers To Temporary Relation

Can I add monikers to my temporary relation?

Yes, but note that relationship manager does not require me to do this. In fact, a temporary relation does not have to have any monikers at all. If a temporary relation does have monikers, they will play no role in how it is linked into the graph. IGraphEvaluation::RemoveRelationshipsAndSubstituteBlock sets up the input and output edges of the temporary relation based solely on the inputs and outputs of the relations that it replaces. Nevertheless, I might want to add monikers to my temporary relationship for my own private purposes. For example, I might use monikers to tell my handler code something about how to evaluate the cycle. In any case, suppose relation R has a moniker M that points to element E. Suppose I want to copy M from R to another relation R'. Moniker M contains a path that must start in the model of R. If R' is not in the same model as R, then it would be incorrect to add M to R', since the starting point would be wrong for the path in M. Now, a temporary relation is in a special temporary model. When copy a moniker from a real relation to a temporary relation, then I run into this problem of moving the starting point for the moniker. I can work around this problem by manipulating the home model ref of the moniker as I copy and then access it.

Here is an example.

// Grab some moniker on a real relation.
Relation rel1 (firstRelInCycle, NULL);
Moniker const& mon = rel1.BeginMonikers().GetMoniker ();
// 1) First, I must satisfy the rule that moniker and relation must have the same home model ref.
// To do this, I just set the moniker's home model ref. It's not correct, but I will compensate for this elsewhere.
Moniker monWithFakeHomeModelRef (mon);
monWithFakeHomeModelRef.SetHomeModelRef (eh.GetModelRef());
// 2) Now I can add the "tweaked" moniker to the temporary relation
tempRel.AddMoniker (NULL, (ParameterId)0, monWithFakeHomeModelRef);
tempRel.GetElemHandle().ReplaceInModel ();

Later on ...

// 3) Retrieve the moniker that I added to the temporary relation:
ParameterQuery monQuery (tempRel, (ParameterId)0);
Moniker mon = monQuery.GetMoniker ();
// 4) At this point, mon's home model ref will match rel's. That is, it will be a model in the temporary file.
// That's wrong, since I know that the moniker was originally created relative to active model.
// Before I can EVALUATE the moniker, therefore, I must set the correct home model ref.
mon.SetHomeModelRef (ACTIVEMODEL);
// 4) Now I can evaluate the moniker:
printf ("Moniker = %ls\n", fmtElementRef (mon.EvaluateElement().GetElementRef().c_str());

Using Relationship::Expression to parse and evaluate C expressions

The Expression class can be used to parse and evaluate a C expression. The expression can contain references to GlobalVariables.

Lifecycle

The Expression class allows you to parse a C expression once and then reevaluate it many times after that. You create an Expression object, ask it to parse the C expression, and then you store it. When you want to evalute the expression, you load the Expression object and call its Evaluate method. You don't have to store and load an expression object in between parsing and evaluating.

Store/Load To store an Expression objectm, call its Store method. That will write out a stream of bytes. It is up to you to save that stream of bytes somewhere and to remember its length. To re-creat an Expression object, call its Load method, passing the stream of bytes that you saved. After calling Load, the expression ready to evaluate.

EnterGraph To add the expression’s inputs to the graph, the EnterGraph method of your relationship class should call the Expression's EnterGraph method.

Monikers To add the expression’s inputs as monikers to your relationship element, you should call the addMonikers function shown below.

Parsing

To parse an expression, call one of the Expression::Parse… methods. For simple expressions that contain no assignments, call ParseNoAssignmentsCExpression.

The Expression parsing methods return an error object if there is a syntax error or if the expression refers to a GV that is not found in the dictionary.

An expression follows C syntax. The usual arithmetic operators are supported. A number of built-in functions such as sin, cos, and tan are also supported. See RelationshipExpressionToken.h for the list of built-in functions. The keywords PI, true, and false are built in as well. There are also a number of extensions built into the Expression evaluator that go beyond C expressions, as described below.

Quantities have units. Distances are stored in UORs and angles are stored in radians. The user may enter these values in master units and degrees, as long as the units are specified. In many cases, the expression parser can recognize the units. For example, if the user inputs “1:10” the parser will recognize this as MU:SU and will convert to UORs. Or, if the user enters “90 deg”, the parse will recognize this as degrees and will convert to radians. The following units keywords are recognized: deg, mu, musq (master units squared), mucu (master units cubed). When calling the Parse methods, you can specify the default units to apply when the user does not specify units.

The parser checks for units mismatches. For example, it’s illegal to cannot add a distance (mu) to an angle (deg).

An expression can contain references to GlobalVariables. When parsing, you must specify the GlobalVariableDictionary that you used to create the GVs.

Evaluating

Call the Expression::Evaluate method to get the value of an expression. Evaluate returns an OperandToken object. You can interrogate this object to find out what type it is. You can then extract its primitive (or extended) value.

using namespace Bentley::MstnPlatform::Relationship;
// -------------------------------------------------------------------------------------------
// This is an overview of the code that you must must write to work with an expression
// -------------------------------------------------------------------------------------------
// 1. When you first receive a C expression from the user
// Create an empty expression object
Expression expression (NULL, getTargetModel(), getTargetDictionary());
if (parseRvalueExpression (exprssion, inputString, defUnits, true) != SUCCESS)
return ERROR;
// store the expression object
DataOutput sink;
expression.store (sink);
… add sink.getBuf() to your relationship element
ParameterID nextParameterId = ??
addMonikerFromExpression (apModelingRelation, expression, getTargetModel(),getTargetDictionary(), nextParameterId);
… ------------// Later // ----------------
// 2. When you want to evaluate the C expression
// Load the expression object
DataInput source (bytes, nBytes);
Expression expression (NULL);
if (expression.Load (source) != SUCCESS)
return ERROR;
RefCountedPtr<OperandToken>* resultSmartPtr
if (evaluateRvalueExpression (resultSmartPtr, expression, true) != SUCCESS)
return ERROR;
// Extract the primitive value from the result
// Check that the type of the value is what you require.
RefCountedPtr<DoubleValue> dbl ( tokenOperand_cast<DoubleValue> (NULL, resultSmartPtr.get()), false );
if (dbl == NULL)
dbl->m_value is a double
…
// -------------------------------------------------------------------------------------------
// These are some supporting functions that you can use
// -------------------------------------------------------------------------------------------
// Parse/tokenize an expression. Detect invalid syntax and unknown GV names
StatusInt parseRvalueExpression
(
Expression& expression,
WChar const* wunp,
Units defUnits,
bool popupOK
)
{
OperandToken* result;
terr = expression.ParseNoAssignmentsCExpression (wunp, VISIBILITY_CALCULATOR, g_mdlDesc, defUnits, NULL);
if (terr.IsError ())
{
mdlOutput_messageCenter (OutputMessagePriority::Warning, const_cast<WChar*>( terr.Fmt().c_str()), NULL, popupOK);
return ERROR;
}
return SUCCESS;
}
// Evaluate an expression
StatusInt evaluateRvalueExpression
(
RefCountedPtr<OperandToken>* resultSmartPtr,
Expression& expression,
bool popupOK
)
{
terr = expression.Evaluate (&result);
if (terr.IsError ())
{
mdlOutput_messageCenter (OutputMessagePriority::Warning, const_cast<WChar*>( terr.Fmt().c_str()), NULL, popupOK);
return ERROR;
}
// Note: As per rules, expression.Evaluate called AddRef on result before returning it.
// We want the UNDERLYING value, not any l-value
RefCountedPtr<OperandToken> rvalue ( result->GetValue (&terr), false ); // As per rules, GetValue calls AddRef on its return value before returning it. smart ptr's dtor will call Release
if (rvalue == NULL)
{
mdlOutput_messageCenter (OutputMessagePriority::Warning, const_cast<WChar*>( terr.Fmt().c_str()), NULL, popupOK);
return ERROR;
}
*resultSmartPtr = rvalue;
result->Release ();
return SUCCESS;
}
SemiPrivate DgnModelRefP getTargetModel ()
{
if (<we are editing a cell>)
return s_targetCell.GetModelRef();
return ACTIVEMODEL;
}
SemiPrivate DgnPlatform::ElementHandle getTargetDictionary ()
{
GlobalVariableDictionary gvd;
if ((<we are editing a cell>)
gvd.OpenCellDictionary (s_targetCell);
else
gvd.OpenModelDictionary (ACTIVEMODEL, true);
return gvd.GetDictionaryElement();
}
void addMonikerFromExpression (Relation& myRel, Expression const& expr, DgnModelRefP model, GlobalVariableDictionary* dict, ParameterId& nextMonikerId)
{
ElementRefSet targets;
expr.GetAllElementRefs (&targets, NULL);
for (ElementRefSet::const_iterator iRef = targets.begin (); iRef != targets.end (); ++iRef)
{
ElementRefP ref = *iRef;
Moniker mon (model, DgnPlatform::PersistentElementPath (model,ref));
rec.AddMoniker (NULL, nextMonikerId, mon);
++nextMonikerId;
}
// Add persistent link to dictionary. This makes the expression part of a copy-able component.
Moniker mon (model, DgnPlatform::PersistentElementPath (model, dict->GetDictionaryElement().GetElementRef()));
myRel.AddMoniker (NULL, nextMonikerId, mon);
++nextMonikerId;
}

Extended Types

An expression can evaluate simple C expressions involving arithmetic operators and built-in functions such as sin, cos, and tan. Expression syntax also supports a number of extended type, including points, vectors, rotation matrices, strings, and arrays. See RelationshipExpressionToken.h for the set of supported types. There are a number of builtin functions to create and manipulate these types. For example, Vector3d creates a vector value, and VectorDotProduct computes the dot product of two vectors.

A GV can hold a primitve C type, such as a double, or it can hold an extended type, such as Point, Vector, etc.

The main purpose of these extended types is to allow you to work with element properties.

Using Expression and GVs together, the user can do something like this:

GVs

a = “Vector3d (1, 0, 0)”

b = “Point3d (0, 0, 0)”

Note: Use Expression to parse and evaluate the above (constant) expressions when creating the GVs!

Expression

“PointAddVector (b, VectorScale (a, 10.0))”

The expression above scales the vector a by 10 and then adds it to the point b. The value of the expression is a new point.


Copyright © 2017 Bentley Systems, Incorporated. All rights reserved.