Sample { array of ints = 1 2 3 5 MATRIX matrix of ints = (1 2 3 1 2 3) some string = blah blah blah sample subtree { #\ a comment element { name = foo } #\ another comment element { name = bar } } a double = 0.3 #\ with a comment } ------- Building the tree ------------------------- #include "QUB_Tree.h" ... // creating the root node: QUB_Tree tree = QUB_Tree::Create("Sample"); // accessing a child node by name -- it will be created and appended if it doesn't exist QUB_Tree child = tree["array of ints"]; // putting data in the child node -- initializing with an array int *initialData = [1, 2, 3, 5]; child.setNumData( QTR_TYPE_INT, 4, 1, initialData ); // (type, rows, cols, initializers or NULL) // adding a node with matrix data (reusing the QUB_Tree variable) child = tree["matrix of ints"]; child.setNumData( QTR_TYPE_INT, 2, 3, NULL ); // filling the data for ( int i=0; i<matrixnode.dataRows(); ++i ) for ( int j=0; j<matrixnode.dataCols(); ++j ) child.dataAs( i, j, (int)0 ) = j; 1 // adding a node with string data tree["some string"].setData( "blah blah blah" ); // adding a subtree 2 QUB_Tree subtree = tree["sample subtree"]; child = subtree.appendChild( "element" ); child["name"].setData( "foo" ); // cloning an "element" child = subtree.appendClone( child ); child["name"].setData( "bar" ); // inserting a comment line (which is represented as a node) // as the first child subtree.insertChild( QUB_Tree::CreateComment( " a comment" ) ); // and after the first "element" subtree.insertChild( subtree["element"], QUB_Tree::CreateComment( " another comment" ) ); // adding a node with one double as data child = tree["a double"]; child.setData( QTR_TYPE_DOUBLE, 0.3 ); 3 child.setLineComment(" with a comment"); -------- Saving and reopening --------------------------------- // saving in binary -- the file remains open until you close it if ( ! tree.saveAs("sample.qtr") ) printf("can't save\n"); // saving a copy as text -- the binary file remains open if ( ! tree.saveTextCopy("sample.qtt") ) printf("can't save text\n"); // do some modifications here ... tree.insertChild("abc").setData( QTR_TYPE_INT, 123 ); // saving changes to disk tree.save(); // closing the binary file tree.close(); 4 // reading the text file tree = QUB_Tree::ReadText("sample.qtt"); // opening the binary file for reading and writing tree = QUB_Tree::Open("sample.qtr"); if ( tree.isNull() ) printf("couldn't open\n"); -------- Reading and modifying data --------------------------------- // reading some data string someString = tree["some string"].dataAsString(); double aDouble = tree["a double"].dataAsDouble(); // reading some data that totally doesn't exist (returns the given default of 1.0) double bDouble = tree["b double"].dataAsDouble( 1.0 ); // raw array access child = tree["array of ints"]; int *iarr = (int *) child.data(); for ( int i=0; i<child.dataCount(); ++i ) printf("%d, ", iarr[i]); // read/write a row of data from a buffer child = tree["matrix of ints"]; vector<int> ivec( child.dataCols() ); child.readDataInto( &(ivec[0]), 0, 0 ); child.writeDataFrom( &(ivec[0]), 1, 1 ); // replacing data child = tree["pqz"]; child.setData( "foo" ); child.setNumData( QTR_TYPE_DOUBLE, 3, 2 ); // resizing the number of rows (so it becomes 4x2) child.resizeData( 4 ); // removing data child.clearData(); -------- Working with large data -------------------------------------- In this section we create a QDF file, but never is the entire data in memory at once. QUB_Tree qdf = QUB_Tree::Create("DataFile"); if ( qdf.saveAs("sample.qdf") ) { // note that saveAs is the first step; // this establishes storage so we can selectively load data // set up structure and metadata QUB_Tree segment = qdf["Segments"]["Segment"]; segment["StartTime"].setData( QTR_TYPE_DOUBLE, 0.0 ); QUB_Tree channels = segment["Channels"]; qdf["Sampling"].setData( QTR_TYPE_DOUBLE, 5.0e-5 ); qdf["Scaling"].setData( QTR_TYPE_DOUBLE, 320.0 ); qdf["ADChannelCount"].setData( QTR_TYPE_INT, 2 ); qdf["ADDataSize"].setData( QTR_TYPE_INT, 2 ); qdf["ADDataType"].setData( QTR_TYPE_INT, QTR_TYPE_SHORT ); // fill channels with 2 channels of 5 megapoints each; // each row is a sample; each column is a channel; // set PRELOAD to false first, so no RAM is allocated. channels.setPreload( false ); channels.setNumData( QTR_TYPE_SHORT, 5000000, 2 ); // in a fit of lameness, just set the data points to zero, 100000 samples at a time for ( int firstRow = 0; firstRow < 5000000; firstRow += 100000 ) { channels.loadData( firstRow, firstRow + 100000 - 1, false ); // false: don't bother reading the (uninitialized) contents short *mplexData = (short *) channels.data(); // alternating channel 0 with channel 1 for ( int i=0; i<200000; ++i ) mplexData[i] = 0; } channels.unloadData(); qdf.save(); } else { printf("can't save as sample.qdf"); } -------- Using QUB_TreeIter to access children ------------------------ Stylistically a QUB_TreeIter resembles an STL iterator: * it points to a child node, or to "the end" * dereference it (*iter) to get a QUB_Tree * ++iter to point to the next child QUB_TreeIter is bidirectional. QUB_TreeMonoIter is forward-only and can't insert or remove children, but is more efficient. QUB_TreeIter chi = tree.children(); 5 if ( (*chi) != tree["array of ints"] ) printf("the first child is not what I expected\n"); // printing the name of each child node for ( chi = tree.children(); ! chi->isNull(); ++chi ) printf("%s\n", chi->name()); // removing [the first child named] "some string" chi = tree.find("some string"); chi.remove(); // now chi points at "sample subtree" // inserting two "x" nodes in its place chi.insert( QUB_Tree::Create("x") ); // now chi points at "x" ++chi; // now chi points at "sample subtree" chi.insert( QUB_Tree::Create("x") ); // checking whether there is a child named "sue" if ( tree.find("sue")->isNull() ) printf("there is not\n"); // printing the "name" of each "element" in "sample subtree" subtree = tree["sample subtree"]; for ( QUB_TreeMonoIter mchi = subtree.find("element"); ! mchi->isNull(); mchi.next("element") ) printf( "%s\n", (*mchi)["name"].dataAsString() ); // appending child nodes more efficiently chi = subtree.end(); chi.insert("element"); (*chi)["name"].setData("aa"); ++chi; chi.insert("element"); (*chi)["name"].setData("bb"); ++chi; --------------------------------------------------------------------------- // Making a copy of the whole tree QUB_Tree treecopy = tree.clone(true); // or just the node and its data QUB_Tree treecopy = tree.clone(false); ----------- Working with QTR_Impl* --------------------------------- class QUB_Tree acts as a wrapper around struct QTR_Impl *. QUB_Tree is more convenient and less error prone, but sometimes you need to pass or store qub trees as pointers. // a function of a tree, returning a tree QTR_Impl * func( QTR_Impl *treeptr ) { // wrap treeptr in a QUB_Tree for easy manipulation QUB_Tree tree( treeptr ); QUB_Tree clone = tree.clone(); // get a pointer for the clone, and incref it so it outlasts the QUB_Tree variable QTR_Impl *cloneptr = clone.getImpl(); QTR_INCREF( cloneptr ); return cloneptr; } // calling func QUB_Tree tree = QUB_Tree::Create("tree"); QTR_Impl *resultPtr = func( tree.getImpl() ); QUB_Tree result( resultPtr ); QTR_DECREF( resultPtr ); In general, you should pass a "borrowed reference", and return a "new reference". On receiving the new reference, wrap it in a QUB_Tree, then decref to release the new ref. // storing a qub tree in a void* static void *whatever; whatever = tree.getImpl(); QTR_INCREF( (QTR_Impl*) whatever ); // using "whatever" QUB_Tree xtree( whatever ); ... // releasing whatever QTR_DECREF( (QTR_Impl*) whatever ); whatever = NULL; --------------------------------------------------------------------- C++'s strong typing can make working with node data kind of a pain. Here are some conveniences: - The "data as" family, for reading the first data item. Conversions are automatic, and if there's no data they will return their argument (default 0). int i = node.dataAsInt(-1); // if no data, it will return -1 double d = node.dataAsDouble(); string s = node.dataAsString(); // if no data, this returns an empty string // Reading "a double" printf( "%f", tree["a double"].dataAsDouble() ); - T& dataAs( int i, T dummy ) T& dataAs( int row, int col, T dummy ) short &sh = node.dataAs( 0, (short) 0 ); - getDataAs*: fills your array, converting as necessary int *buf = new int[ node.dataRows() * node.dataSize() ]; node.getDataAsInts( buf, 0, node.dataRows() - 1 );
1 T& dataAs( int i, T dummy ); T& dataAs( int row, int col, T dummy ); These return a reference that's essentially a pointer into the node's data, so you can assign to it. T must match node.dataType(). The dummy argument specifies the return type but is otherwise ignored. 2 appendChild is rather inefficient: it crawls through the chain of children to find the end, each time. A more efficient way to add several children is shown in the iterator section 3 When calling setData( QTR_TYPE, T ), T must match TYPE exactly. For example, to set the data as a single float: node.setData( QTR_TYPE_FLOAT, (float) 0.3 ); 4 Recall that nodes are reference-counted: they are released when the last reference expires. The flip side is that nodes stay valid as long as they are referenced, even if the file has been closed. Not a big deal when all the data is in memory anyway, but for files with large partially-loaded data you could end up with the whole thing in RAM by mistake. Unfortunately, the variable 'QUB_Tree tree' itself counts as a reference to the underlying root node, so calling tree.close() guarantees the whole thing will stay in memory. To avoid this problem, don't call close(). Files are closed automatically when no variables reference them, so either let the variables go out of scope, or assign the null node: child = QUB_Tree(); subtree = QUB_Tree(); tree = QUB_Tree(); Notice that the root was the last to go. Otherwise subtree would still have been referenced when the file closed, and would have jumped into RAM in order to stay valid. 5 QUB_TreeMonoIter is a more efficient substitute for QUB_TreeIter, at the cost of --, prev(), insert(), and remove()