Node Metadata Systems Analysis
This document provides an overview of the existing data structures used for storing node metadata in CTrack V5.0. The goal is to identify commonalities and design a unified metadata system that can serve as a mini-database for each node.
Executive Summary
CTrack currently uses four distinct systems for storing node metadata:
| System | Purpose | Location | Observer Support |
|---|---|---|---|
| CFeature | Measurement results with units | CTrack_Data/feature.h | No |
| CPropertyBase | Serializable configuration properties | CTrack_Data/DataProperty.h | No |
| ObservableProperty<T> | Observable typed properties | CTrack_Data/ObservableProperty.h | Yes |
| Direct member variables | Colors, file paths, UUIDs | Various classes | No |
1. CFeature System
Class Diagram
Usage Locations
| File | Usage | Feature Types |
|---|---|---|
| CalcDoorslam.cpp | Door slam analysis results | Angle, Position, Velocity, Acceleration |
| CalcTrajectory.cpp | Trajectory deviation | Max deviation (mm) |
| CalcGeomFit.cpp | Geometry fitting results | Residual, NumPoints |
| CalcAlignNPoint.cpp | Alignment quality | Max Dev, St Dev |
| Alignment_*.cpp | Geometry dimensions | Radius, Width, Height, Length, Angle |
| CalcPolyFit.cpp | Polynomial coefficients | C0, C1, C2... |
Feature Value Types
XML Serialization
Features are serialized as a single attribute in the node's XML:
<Node features="Name1=Unit1:Value1;Name2=Unit2:Value2;..."/>
2. CPropertyBase System
Class Diagram
Usage Pattern
The CPropertyBase system is primarily used for:
- Serializing configuration settings to XML
- Populating BCG property windows for user editing
- String-based value conversion
XML Output Format
<PROPERTY name="PropertyName" value="PropertyValue"/>
3. ObservableProperty System
Class Diagram
Observer Integration
4. External File References
Current Implementation
External files are stored as simple CString member variables with manual path conversion:
Path Handling Pattern
// Writing to XML - convert to relative CString RelativePath = makepathrelative(GetPgmPtr(), m_FilePath); GetSetAttribute(pXML, ATTRIB_FILE_PATH, RelativePath, Read); // Reading from XML - convert to absolute m_FilePath = makepathabsolute(GetPgmPtr(), RelativePath);
Files Referenced
| Class | Member | File Types | Purpose |
|---|---|---|---|
| CCalcTrajectoryNominal | m_FilePath | .igs, .step | Nominal trajectory |
| CDeviceOutputFile | m_FilePath | .txt, .csv | Data export |
| CDeviceOutputUncertainty | m_FilePath | .txt | Uncertainty report |
| CCreaformModel | m_ModelPath | Creaform model | 3D scanner model |
| C3DOCC | m_CADFile | .stp, .igs | CAD visualization |
5. Color Properties
5.1 Current Implementation
5.2 Current Color Usage
| Class | Member | Purpose |
|---|---|---|
| C3DOCC | m_Color | 3D object display color |
| CCalcTrajectoryNominal | m_FixColor | Fixed trajectory color |
| CTriggerVisibility | m_ColorSchemeVisible | Visibility indicator colors |
5.3 Advanced Color System Design
The unified NodeProperty system introduces an advanced color model that supports multiple color sources and inheritance.
Color Source Types
Color Source Behavior
| Source | Description | Use Case |
|---|---|---|
| Own | Use the explicitly set RGBA color | Default for most nodes |
| FromFile | Use colors embedded in external file (e.g., CAD) | CAD files with material colors |
| HeatMap | Calculate color from data channel value | Measured trajectories, deviation display |
| Master | Inherit color from ancestor node | Test comparison, grouped visualization |
Color Resolution Flow
Master Color Inheritance
The Master color concept allows hierarchical color inheritance for comparing grouped data:
Key behavior:
- Child nodes with Source: Master display using their ancestor's master color
- The node's own color (OwnColor: Blue) is preserved but not displayed
- Switching back to Source: Own restores the individual color
- Master color lookup traverses up the tree until a node with IsMasterColorProvider = true is found
Color Property Structure
struct ColorProperty
{
// Core RGBA value (always stored, even when using other sources)
uint8_t red = 128;
uint8_t green = 128;
uint8_t blue = 128;
uint8_t alpha = 255;
// Color source selection
ColorSource source = ColorSource::Own;
// Heat map configuration (when source == HeatMap)
struct HeatMapConfig
{
std::string channelKey; // Property key for value lookup
double minValue = 0.0;
double maxValue = 1.0;
HeatMapPalette palette = HeatMapPalette::Rainbow;
bool autoRange = true; // Auto-calculate min/max from data
};
std::optional<HeatMapConfig> heatMap;
// Master color settings
bool isMasterColorProvider = false; // This node provides master color to descendants
// Methods
COLORREF GetEffectiveColor(CNode* context) const;
COLORREF GetOwnColor() const;
void SetOwnColor(COLORREF color);
void SetSource(ColorSource src);
};
XML Serialization for Colors
<!-- Simple own color -->
<C key="DisplayColor" rgba="#FF5733FF" source="own"/>
<!-- Use colors from CAD file -->
<C key="DisplayColor" rgba="#808080FF" source="fromFile"/>
<!-- Heat map based on deviation channel -->
<C key="DisplayColor" rgba="#808080FF" source="heatMap">
<HeatMap channel="Deviation" min="0.0" max="5.0" palette="blueToRed" autoRange="true"/>
</C>
<!-- Inherit from master -->
<C key="DisplayColor" rgba="#0000FFFF" source="master"/>
<!-- Master color provider node -->
<C key="DisplayColor" rgba="#FF0000FF" source="own" isMaster="true"/>
API Examples for Colors
// Set a simple color
node->GetPropertyDB().SetColor("DisplayColor", RGB(255, 87, 51));
// Configure to use CAD file colors
auto* colorProp = node->GetPropertyDB().Get("DisplayColor");
colorProp->SetColorSource(ColorSource::FromFile);
// Configure heat map coloring
colorProp->SetColorSource(ColorSource::HeatMap);
colorProp->SetHeatMapConfig({
.channelKey = "Deviation",
.minValue = 0.0,
.maxValue = 5.0,
.palette = HeatMapPalette::BlueToRed,
.autoRange = true
});
// Set up master color inheritance
testNode->GetPropertyDB().Get("DisplayColor")->SetMasterColorProvider(true);
childNode->GetPropertyDB().Get("DisplayColor")->SetColorSource(ColorSource::Master);
// Get the effective display color (resolves source)
COLORREF displayColor = node->GetPropertyDB().GetEffectiveColor("DisplayColor");
// Get the stored own color (ignores source)
COLORREF ownColor = node->GetPropertyDB().GetColor("DisplayColor");
Color Source Selection UI
6. NODE_UUID References
Design Decision: Node references (UUIDs) remain as separate member variables in their respective classes. They are NOT included in the unified NodeProperty system because:
- They represent structural relationships in the node tree
- They are managed by the existing dependency and parent/child systems
- They have specific semantics (deletion cascade, copy behavior) that differ from properties
Reference Types
Reference Storage Patterns
| Pattern | Example | Purpose |
|---|---|---|
| Single UUID | m_UUID6DOFCalibration | Direct node reference |
| UUID Vector | m_arUUIDMarkers | Multiple related nodes |
| Dependency array | m_arDependingOnUUID | Deletion cascade |
| Feature UUID | CFeature.m_UUID | Back-reference to owner |
These patterns remain unchanged in the new architecture.
7. Data Flow Overview
8. Unified Metadata Design Considerations
Requirements Matrix
| Requirement | CFeature | CPropertyBase | ObservableProperty | Proposed Unified |
|---|---|---|---|---|
| Value + Unit | ✅ | ❌ | ❌ | ✅ |
| Text values | ✅ | ✅ | ✅ | ✅ |
| XML persistence | ❌ (manual) | ✅ | ❌ | ✅ |
| Property window | ❌ | ✅ | Partial | ✅ |
| Change notification | ❌ | ❌ | ✅ | ✅ |
| Type safety | ❌ | Template | ✅ | ✅ |
| Validation | ❌ | ❌ | ✅ | ✅ |
| External file tracking | ❌ | ❌ | ❌ | ✅ |
| Color support | ❌ | ❌ | ❌ | ✅ |
| Batch updates | ❌ | ❌ | ✅ | ✅ |
9. Unified NodeProperty System Design
9.1 Design Philosophy
The unified NodeProperty system provides each CNode with a mini-database of typed metadata that is:
- Observable: Integrates with the existing Observer pattern for change notifications
- Backwards Compatible: Reads legacy CFeature and CPropertyBase XML formats
- Efficient: Writes optimized, grouped XML format
- Flexible: Supports system properties, user-defined properties, and various value types
- Unit-Aware: Stores base units only; UI handles conversion and display
- Keyed Access: Database-like access by type and name
9.2 Property Value Types
9.3 Property Flags and Attributes
9.4 NodeProperty Class Design
9.5 NodePropertyDatabase Class Design
The NodePropertyDatabase serves as the mini-database for each node, providing keyed access by type and name:
9.6 Integration with CNode
9.7 Observer Pattern Integration
The NodePropertyDatabase integrates with the existing Observer pattern through PropertySubject. This builds on the Observer pattern infrastructure documented in Doc/ObserverPattern.md.
PropertyObserverObserverManagerPropertySubject/CNodeNodePropertyNodePropertyDatabaseApplication CodePropertyObserverObserverManagerPropertySubject/CNodeNodePropertyNodePropertyDatabaseApplication Codealt[Not in batch mode][In batch mode]SetNumeric("MaxDev", 0.5, "mm")Find or create propertyUpdate valueReturn old valueNotifyPropertyChange("MaxDev", old, new)Dispatch(PropertyChange)OnChange(PropertyChange)Update UI/ViewQueue change for batch notification
PropertyChange Event Structure
The PropertyChange event (defined in ChangeTypes.h) carries all information needed for observers:
struct PropertyChange
{
PropertyChangeType type; // ValueChanged, PropertyAdded, PropertyRemoved, etc.
NODE_UUID nodeId; // Which node's property changed
std::string propertyName;
std::string propertyPath; // For nested: "Settings.Display.Color"
std::any oldValue;
std::any newValue;
std::string propertyType; // For UI type hints
bool isBatchUpdate = false;
};
Observer Categories
Properties integrate with the existing category system:
Batch Updates
For bulk property changes (e.g., loading from XML or recalculation), batch mode prevents notification storms:
// Batch update example - single notification at end
node->GetPropertyDB().BeginBatch();
node->SetFeature("MaxAngle", "deg", maxAngle);
node->SetFeature("Overshoot", "mm", overshoot);
node->SetFeature("MaxVelocity", "mm/s", maxVel);
node->SetFeature("MaxAcceleration", "mm/s2", maxAcc);
node->GetPropertyDB().EndBatch(); // Single batched PropertyChange notification sent here
The batch notification uses the existing OnBatchChanges() mechanism from the Observer base class, allowing observers to decide between incremental updates or full rebuild.
9.8 Typed Access Methods
The database provides strongly-typed access with compile-time type checking:
// Type-safe numeric access - always returns base unit value
double maxDev = propDB.GetNumeric("MaxDeviation", 0.0); // Returns in base unit (mm)
// Type-safe with category filtering
auto features = propDB.GetByCategory(PropertyCategory::Feature);
for (auto* prop : features) {
if (prop->HasUnit()) {
// UI converts from base unit to display unit using CUnits
double displayValue = GetUnits()->Convert(prop->GetNumeric(), prop->GetBaseUnit());
}
}
// User-defined property access
auto userProps = propDB.GetUserDefined();
9.9 XML Serialization
New Optimized Format (Writing)
When writing, properties are grouped by category for efficient parsing:
<Node name="DoorSlamCalc" uuid="12345">
<Properties version="2">
<!-- Features grouped together -->
<Features>
<F key="MaxAngle" unit="deg" value="145.3"/>
<F key="Overshoot" unit="mm" value="2.1"/>
<F key="MaxVelocity" unit="mm/s" value="1250.0"/>
</Features>
<!-- Settings grouped together -->
<Settings>
<S key="TrimAngle" type="double" value="5.0"/>
<S key="FilterEnabled" type="bool" value="true"/>
</Settings>
<!-- External files - paths stored as relative -->
<Files>
<File key="TrajectoryFile" path="data/nominal.igs"/>
<File key="CADModel" path="models/door.step"/>
</Files>
<!-- Colors stored as hex -->
<Colors>
<C key="DisplayColor" value="#FF5733"/>
</Colors>
<!-- User-defined properties -->
<UserDefined>
<U key="CustomerNote" type="text" value="Approved by QA" visible="true"/>
<U key="TestPriority" type="int" value="1" readonly="false"/>
</UserDefined>
</Properties>
</Node>
Legacy Format Reading (Backwards Compatibility)
The system transparently reads legacy formats:
<!-- Legacy CFeature format (version 1) -->
<Node features="MaxAngle=deg:145.3;Overshoot=mm:2.1"/>
<!-- Legacy CPropertyBase format -->
<Node>
<PROPERTY name="TrimAngle" value="5.0"/>
</Node>
<!-- Mixed legacy format -->
<Node features="Radius=mm:50.0" filepath="models/part.step" color="255,128,0">
<PROPERTY name="Enabled" value="true"/>
</Node>
The ReadXML() method detects the format version and dispatches to appropriate parsers:
bool NodePropertyDatabase::ReadXML(TiXmlElement* pXML, const CString& projectRoot)
{
// Check for new format
TiXmlElement* pProperties = pXML->FirstChildElement("Properties");
if (pProperties)
{
int version = 1;
pProperties->Attribute("version", &version);
if (version >= 2)
return ReadXML_V2(pProperties, projectRoot);
}
// Fall back to legacy format reading
CString featuresAttr;
if (GetAttribute(pXML, "features", featuresAttr))
ReadLegacyFeatures(featuresAttr);
// Read legacy PROPERTY elements
for (auto* pProp = pXML->FirstChildElement("PROPERTY"); pProp;
pProp = pProp->NextSiblingElement("PROPERTY"))
{
ReadLegacyProperty(pProp);
}
return true;
}
9.10 Property Flags Detail
| Flag | Description | Default | Persisted |
|---|---|---|---|
| Visible | Show in UI (property window, grids) | true | Yes |
| ReadOnly | Cannot be modified by user | false | Yes |
| UserDefined | Created by user, not system | false | Yes |
| Persistent | Save to XML | true | N/A |
| ShowInPropertyWindow | Display in BCG property window | true | No |
| ShowInFeatureGrid | Display in feature overview grid | Category-dependent | No |
| Exportable | Include in CSV/Excel exports | true | No |
9.11 Unit Handling
Important Design Decision: Properties store base units only (e.g., mm, deg, mm/s). The UI layer is responsible for:
- Retrieving the user's preferred display unit from CUnits
- Converting values for display using GetUnits()->GetConversionFactor()
- Converting user input back to base units before storing
This matches the existing CFeature behavior where GetValue(true) returns base unit and GetValue(false) applies conversion.
Base units are stored as strings matching the existing unit constants:
| Unit Type | Base Unit | Example Display Units |
|---|---|---|
| Length | mm | inch, m, ft |
| Angle | deg | rad, arcmin |
| Velocity | mm/s | m/s, inch/s |
| Acceleration | mm/s2 | m/s², g |
| Time | s | ms, min |
| Temperature | C | F, K |
9.12 User-Defined Properties
Users can add custom metadata to any node. These properties:
- Are flagged with PropertySource::User and UserDefined = true
- Are always persisted to XML
- Can be marked visible/hidden and readonly/editable
- Support all value types
9.13 Complete Class Hierarchy
10. API Examples
10.1 Feature Operations (Replacing CFeature)
// Old CFeature way
SetFeature("MaxAngle", CFeature(UNIT_ANGLE, maxAngle));
double val = GetFeatureValue("MaxAngle", true); // base unit
// New NodeProperty way - simple form
GetPropertyDB().SetNumeric("MaxAngle", maxAngle, UNIT_ANGLE);
double val = GetPropertyDB().GetNumeric("MaxAngle", 0.0);
// New NodeProperty way - with full control
NodeProperty prop;
prop.SetNumeric(maxAngle, UNIT_ANGLE);
prop.SetCategory(PropertyCategory::Feature);
prop.SetSource(PropertySource::Calculation);
prop.SetDisplayName("Maximum Opening Angle");
prop.SetDescription("The maximum angle reached during door slam");
prop.SetGroup("Results");
prop.SetVisible(true);
prop.SetReadOnly(true); // Calculation results should not be user-editable
GetPropertyDB().Add("MaxAngle", prop);
10.2 User-Defined Properties
// User adds custom metadata through UI or API
node->AddUserProperty("CustomerID", TextValue("ACME-001"));
node->AddUserProperty("TestPriority", IntValue(1));
node->AddUserProperty("ApprovedBy", TextValue("John Doe"));
node->AddUserProperty("ApprovalDate", TextValue("2024-01-15"));
// Make some properties read-only after setting
node->GetProperty("ApprovedBy")->SetReadOnly(true);
// Query user properties for display in custom property grid
for (auto* prop : node->GetPropertyDB().GetUserDefined()) {
if (prop->IsVisible()) {
AddToUserPropertyGrid(prop);
}
}
10.3 File Path Properties
// Set external file reference
GetPropertyDB().SetFilePath("CADModel", "C:/Projects/CTrack/models/assembly.step");
// On XML save - automatically converts to relative path based on project root
// <File key="CADModel" path="models/assembly.step"/>
// On XML load - automatically converts to absolute path
// Uses projectRoot parameter passed to ReadXML()
// Check if file has been modified externally
auto* prop = GetPropertyDB().Get("CADModel");
if (prop && prop->IsFileModified()) {
// Prompt user to reload CAD model
ReloadCADModel(prop->GetFilePath());
prop->UpdateFileTimestamp();
}
10.4 Iteration and Filtering
// Iterate all properties in insertion order
for (auto& [key, prop] : node->GetPropertyDB()) {
if (prop.IsVisible() && prop.ShowInPropertyWindow()) {
AddToPropertyWindow(prop);
}
}
// Get only exportable features for CSV export
auto features = node->GetPropertyDB().GetByCategory(PropertyCategory::Feature);
for (auto* prop : features) {
if (prop->IsExportable()) {
ExportToCSV(prop->GetKey(), prop->GetNumeric(), prop->GetBaseUnit());
}
}
// Get all visible properties for property window
auto visible = node->GetPropertyDB().GetVisible();
// Get settings that can be edited
auto settings = node->GetPropertyDB().GetByCategory(PropertyCategory::Setting);
for (auto* prop : settings) {
if (!prop->IsReadOnly()) {
AddEditableProperty(prop);
}
}
10.5 Observer Integration Example
class FeatureGridObserver : public CategoryObserver<ChangeCategory::Property>
{
public:
void OnChange(const ChangeEvent& change) override
{
const PropertyChange* propChange = GetChangeAs<PropertyChange>(change);
if (!propChange) return;
switch (propChange->type)
{
case PropertyChangeType::ValueChanged:
// Find cell in grid and update value
UpdateCell(propChange->nodeId, propChange->propertyName, propChange->newValue);
break;
case PropertyChangeType::PropertyAdded:
// Add new row to grid
AddRow(propChange->nodeId, propChange->propertyName);
break;
case PropertyChangeType::PropertyRemoved:
// Remove row from grid
RemoveRow(propChange->nodeId, propChange->propertyName);
break;
case PropertyChangeType::AllPropertiesChanged:
// Full refresh needed
RefreshNode(propChange->nodeId);
break;
}
}
void RedrawView() override
{
m_grid.Invalidate();
}
void RebuildView() override
{
m_grid.Clear();
LoadAllFeatures();
}
bool AcceptsChange(const ChangeEvent& change) const override
{
// Only accept property changes for Feature category
const PropertyChange* propChange = std::get_if<PropertyChange>(&change);
if (propChange)
{
// Could filter by property category here
return true;
}
return false;
}
};
10.6 Batch Processing Example
void CCalcDoorslam::PerformCalculation(CTaskScheduler* pTaskManager)
{
// ... calculation logic ...
// Batch all feature updates - single notification at end
GetPropertyDB().BeginBatch();
SetFeature("MaxAngle", UNIT_ANGLE, MaxOpenAngle);
SetFeature("Angle1", UNIT_ANGLE, Angle1);
SetFeature("Angle2", UNIT_ANGLE, Angle2);
SetFeature("Overshoot", UNIT_MM, Overshoot);
SetFeature("OvershootAngle", UNIT_ANGLE, OvershootAngle);
SetFeature("OvershootWorldX", UNIT_MM, OvershootVectorWorld.r(1, 1));
SetFeature("OvershootWorldY", UNIT_MM, OvershootVectorWorld.r(2, 1));
SetFeature("OvershootWorldZ", UNIT_MM, OvershootVectorWorld.r(3, 1));
SetFeature("Doordrop", UNIT_MM, Doordrop);
SetFeature("MaxVelocity", UNIT_POS_VEL, MaxVelocity);
SetFeature("MaxAcceleration", UNIT_POS_ACC, MaxAcceleration);
GetPropertyDB().EndBatch();
// Single PropertyChange event with isBatchUpdate=true sent here
// Observers can choose to RebuildView() or process individually
}
11. Migration Strategy
Phase 1: Core Implementation
Phase 2: XML Serialization
Phase 3: Observer Integration
Phase 4: Gradual Migration
| Component | Current System | Migration Approach |
|---|---|---|
| CCalcDoorslam | SetFeature() calls | Redirect to GetPropertyDB().SetNumeric() |
| CAlign* geometry | SetFeature() for dimensions | Same |
| C3DOCC | m_Color, m_CADFile members | Move to property database |
| Property windows | CPropertyBase population | Populate from NodePropertyDatabase |
| Feature grid | CFeatureOverview | Query GetByCategory(Feature) |
Phase 5: Deprecation Path
- Mark CFeature methods as [[deprecated]]
- Mark CPropertyBase methods as [[deprecated]]
- Provide compile-time warnings with migration hints
- Remove in future major version (V6.0)
12. Summary
The unified NodeProperty system provides:
| Capability | Implementation |
|---|---|
| Mini-database per node | NodePropertyDatabase with keyed access |
| Type safety | PropertyValue variant with typed accessors |
| Observable changes | Integration with PropertySubject and ObserverManager |
| Backwards compatibility | Legacy XML format readers for CFeature and CPropertyBase |
| Efficient storage | Grouped XML output format (version 2) |
| User extensibility | UserDefined flag and PropertySource::User |
| Visibility control | PropertyFlags bitfield |
| Unit awareness | Base unit storage only; UI handles conversion via CUnits |
| File tracking | FilePathValue with relative/absolute conversion |
| Color support | ColorValue with RGBA, source selection (Own/FromFile/HeatMap/Master), and inheritance |
| Batch operations | BeginBatch()/EndBatch() for bulk updates |
This design unifies CFeature, CPropertyBase, ObservableProperty, and scattered member variables into a single, consistent, observable metadata system that serves as each node's personal database while maintaining full backwards compatibility with existing project files.