Associating Application Data With the HOOPS Database

In this section, we discuss how to set up a correspondence between your application’s data structures and the graphical representation of the data (the scene graph) stored in the HOOPS database.

../../_images/015a_mapping_app_data.gif

Mapping of application data and scene-graph.

For simplicity, in this section, we shall assume that there is only a single active view, but multiple views of the same data model could be handled the same way.

To set up the correspondence between application data and scene-graph, we have to solve two problems:

  1. Given an application data structure, how do you find the portion of the HOOPS database that visually represents that data structure?

  2. Given a segment in the HOOPS graphics database, how do you find the corresponding application data structure?

Note that the correspondence between application data structures and segments in the HOOPS database might not be a one-to-one correspondence. In particular, an application data structure can correspond to an object in the HOOPS database that is made up of more than one segment. In this case, we normally refer to the object in the HOOPS database using the segment at the root of the subtree that represents the object.

Mapping Application Data to the HOOPS Database

There are two ways to set up a mapping between your application’s data structures and the graphical representation stored in the HOOPS database: using segment names or using keys.

Segment Names

Let’s say we want to render a picture of the solar system. We can use a segment to represent each planet:

    HC_Open_Segment("mercury");

…and place the appropriate geometry and attributes for the particular planet in this segment and its subtree. Since all segments representing the spheres would probably have the same parent segment, we could use a single segment name to open the segment for each sphere (assuming that the parent segment had already been opened). In a more complicated model, we might need to store a path name to the segment.

The main advantage of using segment names to map application data to the scene-graph is simplicity. The use of segment names is easy to program and simple to understand. It also makes your application easier to debug, since you can tell from the name of a segment what that segment does. However, there are a couple of disadvantages to using segment names. First, using a segment name requires that you either store the name as a character string in your data model, or reformat the name each time that you need it (as we did in the spreadsheet example). Second, opening a segment by giving a name (especially a path name) is slower than other methods, such as using keys (and can get slower as more segments are added and the number of segment operations increases).

Keys

A more common way to associate application data to the scene-graph is through the use of keys, which were added to HOOPS primarily for this purpose. The key for a segment would be stored in the application data structures (as a long integer). Normally, the integer would be stored in the application data when you initially set up the scene-graph.

In the solar system example, you would therefore initially create each planet segment using the variant of Open_Segment which returns a key, and then store this key in your data structures.

    HC_KEY planet1 = HC_Open_Segment("mercury");

Note that we also specified a meaningful name when creating the segment. If a name wasn’t of interest, we could alternately create a ‘no-name’ segment which is only accessible by its key:

    HC_KEY planet2 = HC_Open_Segment("");

Then, to open the segment, you would use the command

    HC_Open_Segment_By_Key(planet1);

Using this command is about as simple as using segment names, but it does require that you have a key (unless you renumber the keys - something that we shall discuss in the next section).

An additional advantage of using keys is that, unlike names, keys can refer to geometry. This ability allows you to avoid using extra segments. For example, imagine that we are drawing a map of a computer network (such as the Internet). Each computer site is represented as a fixed point, and each connection is represented as a line drawn between two sites. Assuming that all these lines have the same attributes, we do not need to place each line into its own segment. Instead, we can use the key returned from Insert_Line, and can access each line directly.

Renumbering of Keys

HOOPS has a facility that makes it even more convenient to use segment keys to associate application data to the scene graph. Normally, HOOPS assigns a key for each segment (or geometry), but you can change the key value with the Renumber_Key function.

For example, the following program shows how a a method of a ‘Planet’ class might go about creating the graphical representation of a planet in the HOOPS database. The Renumber_Key command renumbers the key of each segment representing a planet to be the planet number.

    HC_KEY old_key;

    HC_Open_Segment("planets");
    old_key = HC_Open_Segment("");
    HC_Renumber_Key(old_key, planet_num, "global");
    // code to set up attributes and geometry for the planet
    HC_Close_Segment();
    HC_Close_Segment();

Renumbering of the key to be the planet number.

Renumbering the key to be the planet number makes accessing a planet convenient. Given the number of a planet segment, you can open it with the HOOPS command Open_Segment_By_Key. For example, to open the segment representing planet number 5, you simply use:

    HC_Open_Segment_By_Key(planet_num);

Renumbering the key removes the need to store the key as a data member in your application data structures.

If your application has some sort of object identifier for each object in its application database (for example, the planet number), then it is convenient to use Renumber_Key to set the HOOPS database key to be the object identifier. You can even renumber a key to be a pointer to a data structure.

Each key in the database must be unique (or HOOPS will complain). You can also specify a key as local, in which case only keys for objects with the same parent segment have to be unique. A disadvantage of using local keys is that the parent segment must already be open before the child can be opened by key.

User-supplied keys must be non-negative (positive or zero). The keys assigned by HOOPS Visualize are always negative.

Using renumbered keys is slightly slower than is using the original key, but is always faster than is using segment names. You can always restore the original key by calling Renumber_Key and specifying the special value -1 (negative one) as the new key.

Mapping Objects in the HOOPS Database to Application Data

Mapping scene-graph objects to application data is primarily necessary when you are handling input events, such as selection events. When the user selects some object on the screen, HOOPS returns to your application the segment and object that was selected. Your application needs a way to map that segment back to an application data structure. For example, the user might select one of the pieces of geometry representing a planet, and we need a way to figure out which planet object the geometry corresponds.

This problem can be summarized as follows: Given a segment or piece of geometry in the HOOPS database, how do you find the application data structure that corresponds to that segment/geometry? Basically, you must store a pointer to your application data structure in the HOOPS database. HOOPS provides several ways to store such a pointer, each one appropriate to a different situation.

User Options

The simplest way to store application-specific information in the HOOPS database is with user options. A user option is an attribute, and behaves just like a normal attribute, except that it is supplied and interpreted by the user. You can have any number of user options; each user option is identified by a name, and has a value. Both the option name and value are strings.

In the solar system example, we can create a user option named “planet number” whose value is the (string representation) of the planet number. For example, in the segment corresponding to planet 5 in the data model, we would store the user option as follows:

    HC_Set_User_Options("planet number=5");

You can retrieve all the user options set on a segment with Show_User_Options (if a segment has multiple user options, they are all returned in a single string, separated by commas). It is usually more useful to show a single user option, for example, the following code returns the value of the “planet number” user option:

    char option_value[128];
    HC_Show_One_User_Option("planet number", option_value);

Since user options inherit just like any other attribute, it is less error prone to return the net value of a user option, rather than the local attribute value.

    HC_Show_One_Net_User_Option("planet number", option_value);

There are two main advantages to showing the net value of a user option. First, a segment has a local attribute value only if the latter was explicitly set on it. If you use Show_One_User_Option and there was no user option “planet number” set on the current segment, then HOOPS will issue a warning. A segment always has a net value for every attribute. The default value for any user option is the empty string (“”). If the user option called “planet number” has no value, then the Show_One_Net_User_Option will return the empty string, rather than issuing a warning.

The second advantage to using Show_One_Net_User_Option is that you will net any inherited value, rather than just a locally set value. For example, if a planet in the solar system is drawn using multiple segments, we need to set a user option on only the parent segment of the subtree representing this planet, and the other segments in the subtree will inherit the value. That way, if the system does a selection event, and the user picks some geometry down inside the segment hierarchy corresponding to a planet, your program will still get back the user option for the appropriate planet number.

In this section, we stored a simple integer value for a planet number (albeit as a string). You can also store a C++ pointer value, by converting it into a hexadecimal string and storing it as a user option.

User Options in Unicode

For some situations, it is more applicable to set user options with Unicode strings. HOOPS supports the UTF-16 character set. To set user options in Unicode, HOOPS provides the function Set_Unicode_Options. To determine if a segment has Unicode user options instead of using Show_User_Options and its related variants, use Show_Existence_Unicode to check if Unicode options exist and then call Show_Unicode_Options to get the user option information. The sample code below shows how these functions are used in conjunction with one another.

    unsigned short planetNumber[] = {'p', 'l', 'a', 'n', 'e', 't', ' ', 'n', 'u', 'm', 'b', 'e', 'r', '=', '5', 0};
    unsigned short userOption[] = {'u', 's', 'e', 'r', ' ', 'o', 'p', 't', 'i', 'o', 'n', 's', 0};
    unsigned short userOptionValues[256];

    HC_Set_Unicode_Options(planetNumber);
    if (HC_Show_Existence_Unicode(userOption))
        HC_Show_Unicode_Options(userOptionValues);

User Data

Sometimes the data that you want to associate with a specific segment is binary data. With the user data functionality, you can link a piece of data with a segment and HOOPS will not process or modify the data. In following sample code, we see how user data is initially set:

    HC_Set_User_Data(index, my_binary_data, data_length);

The Set_User_Data function takes as its parameters an index, a pointer to the binary data and the size of the data. The index value must be a unique integer for each chunk of user data. This allows you to associate multiple chunks of data with a single segment.

To retrieve user data from a segment, use the Show_One_User_Data. This function returns user data that has been set on the currently open segment and associated with a specific index. It takes as its parameters the index, a pointer to a buffer where data can be populated and the size of the buffer. When you query for user data, you might not know what the size of the user data is. However, you can find out by making an initial call to Show_One_User_Data with a buffer of insufficient size and examining the return value. If data is found that is associated with passed index but the buffer passed was too small, the return value will be a negative number that is the size of the user data associated with the index. The following sample code shows how this can be implemented:

    long result = HC_Show_One_User_Data(index, 0, 0);

    if (result < 0) {
        // Data found at index.  Need to allocate correct size.
        long out_size = -result;
        char* data_out = (char*)malloc(out_size);
        result = HC_Show_One_User_Data(index, data_out, out_size);
    }

Keys Mapping

Using Renumber_Key, we can use a single key to map application data to the scene-graph and vice versa, all at the same time. We renumber the segment key to be a pointer to one of our application data structures. Given a segment, we can use its key to access the data structure. Conversely, given the data structure, we can use its address to access the segment by key.

First, however, we need to use a little trick. The problem with using a pointer as a key is that a user-defined key must be positive, and, when a pointer is converted into a signed long integer, it might be either positive or negative. The trick is to recognize that the least-significant bit of a pointer is always zero, because data structures are always aligned on word boundaries and the smallest word boundary of any architecture on which HOOPS runs is 2 bytes (of course, there have been computers in the past that aligned structures on byte boundaries, and someone could always build a computer that will do that again, but we doubt that someone will, since the trend today is toward aligning structures on 4-byte or even 8-byte boundaries). This (relatively obscure) fact allows us to store a pointer as a non-negative signed integer by shifting it one place to the right:

#define POINTER_SIZED_HIGHBIT ((HC_KEY)1 << (8 * sizeof (::KEY) - 1))
#define POINTER_TO_KEY(p) ((((HC_KEY)p)>>1) & ~(POINTER_SIZED_HIGHBIT))

void Planet::CreateGraphicsObject ()
{
        HC_KEY old_key;

        HC_Open_Segment("?Picture/planets");
          old_key = HC_Open_Segment("");
                assert ( (long) my_planet_object_ptr & 0x01) == 0 );
                HC_Renumber_Key(old_key, POINTER_TO_KEY(this), "global");
                // set up attributes and geometry
          HC_Close_Segment();
        HC_Close_Segment();
}

Given a segment key, the function in the following code snippet will return a pointer to the planet object that it represents:

#define KEY_TO_POINTER(k) (k << 1)

Planet* GraphicsObjectToPlanet(HC_KEY key)
{
    if (key < 0)
        return (Planet*)0; // error
    else
        return (Planet*)(((uintptr_t)key) << 1);
}

Mapping of scene-graph to application data using keys.

One problem with using keys to map graphical objects back to application data structures is that keys do not inherit (only attributes inherit). If a selectable object is made up of multiple segments, we need to make sure that all segments in the object map back to the same selected application data structure. When we used a user option to map graphical information to application data, this mapping happened automatically when we set the user option on the root of the segment subtree corresponding to the object, because all the other segments inherited the user option from the root segment. If we perform the mapping by renumbering the key of the root segment of the object, the other segments do not inherit the renumbered key. If the user selects a segment that has not had its key renumbered, the GraphicsObjectToPlanet function will return a null Planet pointer. We cannot solve this problem by renumbering the key of all segments corresponding to the object, since keys must be unique (even if we use local keys, segments that have the same parent must have unique keys).

One way to solve this problem is to modify the GraphicsObjectToPlanet function to check the parent of the current segment (and its parent, recursively) if the current segment’s key was not renumbered (by testing to see whether the key is negative).

Another way to solve this problem is through careful use of the Set_Selectability command to make sure that only segments that have had their key renumbered can be selected. For each object in the HOOPS database that has more than one segment, we renumber the key of the root segment, make the root segment selectable, and make all of its child segments not selectable. When the user selects the object, the selection event will return the lowest segment that is selectable, and that will be the segment whose key was renumbered. Set_Selectability is discussed in the selection section of this guide.

User Values

Before user options were added to HOOPS, you could store a single long integer in a segment using the ::Set_User_Value command. This command is now obsolete. There are two advantages to using a user value instead of a user option: (1) the user value is stored as a long integer, so it does not have to be converted into a string (unlike a user option); and (2) it can be negative (unlike a key).

Advanced Mapping Examples

The previous examples describe a fairly simple situation involving a one-to-one mapping between an application data object and a segment in the HOOPS database. Frequently, more complex mappings are required as the number of graphics objects increase or have varying attributes.

Let’s say that we need to provide a graphical representation of the asteroid belt of our solar system, and each asteroid has a color assigned to it. Since each segment attribute can only have a single value at a time, this requires that we create at least one segment for each color value. We could create a unique segment for each asteroid and directly set the color in the segment, resulting in a one-to-one mapping, but this would be quite inefficient since it would result in excess segments. Recalling that proper HOOPS usage involves placing objects with similar attributes in the same segment, it would be better to create one segment for each color, and then map the graphics representation of each asteroid to the appropriate segment. There are a variety of ways to achieve this many-to-one mapping.

Using Renumbered Keys

We’ve already seen how we can use renumbered keys to map from application data to scene-graph objects. To demonstrate a slightly more complex example of using keys, let’s deal with the situation where each asteroid will be one of 256 colors, and the asteroid color is represented by an index into a color map provided with the asteroid data.

To initialize the segment hierarchy, we would create a segment for each of the color map values, and renumber the keys of the segments to be the index of the color set in the segment. Assuming that the color values reside in an array called ‘color map’, this code snippet sets up the segment hierarchy:

    int i;

    HC_Open_Segment("asteroid_belt");

    for (i = 0; i < 256; i++) {
        old_key = HC_Open_Segment("");

        // set color using the value located at color map[i]
        HC_Renumber_Key(old_key, i, "global");
        HC_Close_Segment();
    }

    HC_Close_Segment();

Renumbering keys based on attribute value

Now that the segment hierarchy has been created, we need to extend the Initialize method to insert geometry into the appropriate segment. Let’s further assume that the asteroid information resides in an array called AsteroidList containing pointers to an Asteroid object, and each object contains a ‘color map_index’ along with other asteroid ‘data’. The following code maps the asteroid information to the Visualize database, and renumbers the asteroid’s geometry to be the pointer to the Asteroid object:

{
        HC_Key old_asteroid_key;
        int i;

        for (i = 0; i < number_of_asteroids; i++)
        {
          // open the segment associated with the asteroid's color map index,
          // insert the geometry,and renumber the geometry key to be the
          // pointer to the Asteroid object

          HC_Open_Segment_By_Key(AsteroidList[i]->color map_index);
                old_asteroid_key = HC_Insert_Shell( < AsteroidList[i]->data > );
                HC_Renumber_Key(old_asteroid_key, POINTER_TO_KEY(AsteroidList[i]), "global");
          HC_Close_Segment();
        }
}

Using a Hash Table

Now let’s take a more complex situation where each asteroid has an RGB color (with each color channel being a value from 0 to 255) rather than a color that indexes into a color map of previously known size. We can’t create a segment for each potential color because that would involve creating 16.7 million segments! Rather, we must follow the guideline of placing similarly colored asteroids into the same segment. This vastly improves performance and reduces memory usage since the number of segments is kept to a minimum. Therefore, we would use a hash table to map the asteroid to the appropriate segment.

From the color value we lookup into the hash table to obtain a segment key. If the table returns a key, then the segment exists and we insert the asteroid geometry into the segment. If no segment key is returned, then we need to create a segment with the new RGB color. The following pseudo-code maps the asteroid information to the HOOPS Visualize database:

{
        HC_Key old_asteroid_key, segment_key;
        int i;
        HashTable * m_pColorToKeyTable = new HashTable();

        for (i = 0; i < number_of_asteroids; i++)
        {
                if ((segment_key = hash_lookup(m_pColorToKeyTable, AsteroidList[i]->color)) == -1)
                {
                        // no segment currently exists with the color required, so we need to create one
                        // and insert the color/key pair into the hash table segment_key = HC_Open_Segment("");
                        // set appropriate color using AsteroidList[i]->color HC_Close_Segment();
                        hash_insert(m_pColorToKeyTable, AsteroidList[i]->color, segment_key);
                }

                // open the segment which contains the required color, insert the geometry,
                // and renumber the geometry key to be the pointer to the Asteroid object
                HC_Open_Segment_By_Key(segment_key);
                  old_asteroid_key = HC_Insert_Shell( < AsteroidList[i]->data > );
                  HC_Renumber_Key(old_asteroid_key, POINTER_TO_KEY(AsteroidList[i]), "global");
                HC_Close_Segment();
        }
}

Attribute Combinations

The previous examples dealt with mapping application data to the HOOPS Visualize scene graph based on a single attribute (color). Combinations of attributes would require more complex mapping functions. For example, let’s say we were visualizing an AEC drawing, which consisted of lines that had various thickness, color and pattern. These display properties are represented by 3 distinct attributes (line weight, color, and line pattern), and therefore the geometry should be mapped into segments which contain the necessary combination of these attributes. Similar to the RGB color example above, the correct approach is to use a hash table rather than to pre-create segments which contain every combination of the three attributes (this would require ~ 16.7 million * 10 * 10 HOOPS segments, which is obviously not a feasible approach).