Implementing custom globe layers with dynamic feed


Development licensing Deployment licensing
Engine Developer Kit Engine Runtime: 3D
ArcView: 3D Analyst ArcView: 3D Analyst
ArcEditor: 3D Analyst
ArcInfo: 3D Analyst

 

Introduction

 
The addition of ArcGlobe to the ArcGIS 9 application suite has enabled a continuous 3D visualization of spatial data ranging from global to local scale. ArcGlobe can efficiently display terabytes of high-resolution images as well as sophisticated 3D objects with photorealistic texture. This capability has drawn significant interest from many users and developers who want to extend the functionality of ArcGlobe to apply in their application domain.
 
Drawing 3D graphics on the globe display has been one of the most frequently asked questions in the ESRI developer community. In many cases, users have asked to connect to a live feed source of data and draw this data on the globe. This data can be virtually anything spatially enabled (starting from airplanes and other vehicles and ending at birds and whales). These graphical objects are points, lines, polygons, texts, images, and 3D models. This document will review the different ways to display such information on the globe.
 
Since ArcGlobe is an OpenGL-based application, the OpenGL API is key to this functionality. OpenGL is a widely used industry standard for drawing 3D graphics. It has been supported on most operating systems and hardware platforms.
 
All code samples in this document were taken from the RSS Weather 3D Layer sample, which can be found in the ArcGIS .NET SDK.
 

Prerequisites

 
This document assumes that the reader has a good programming knowledge of C#, component object model (COM), and OpenGL. The major focus will be the ArcObjects Globe API as well as OpenGL. Reviewing the CSimplePointLayer sample code from the Extending ArcObjects book is also recommended.
 

Drawing simple 3D graphics

 
ArcGlobe has provided 3D rendering functionality by feature layer since its first release in ArcGIS 9. Significant changes have been made in ArcGIS 9.2 to allow the drawing of 3D graphics by user interface and ArcObjects programming.
Manipulating feature layers
Vector data, which is stored as feature classes, can be rendered in 3D through feature layers. In ArcScene and ArcGlobe, feature layers can be represented as 3D drawings using advanced symbology, such as textured multipatches, extruded polygons and polylines, billboard images, and so on. Dynamic drawing is achieved by translating the entire feature layer with the animation framework. Starting at ArcGIS 9.2, it is possible to apply transformation to features belonging to a feature class.
Drawing 3D objects using GlobeGraphicsLayer
In ArcGIS 9.1, the only way to draw 3D graphical objects on a globe display was to perform the drawing programmatically by calling OpenGL functions in the BeforeDraw() or AfterDraw() method of the IGlobeDisplayEvents. In ArcGIS 9.2, many of the 3D objects, including simple and complex 3D graphics, and text can be drawn using the GlobeGraphicsLayer in a similar manner as ArcScene and ArcMap. The Symbol Picker  dialog box shows categories of 3D symbols, for example, buildings, trees, vehicles, and so on. A simple 3D Graphics toolbar similar to the one in ArcScene is available out of the box.
The API to manipulate 3D symbols as well as the GlobeGraphicsLayer is available to developers. This API allows you to easily shift, rotate, and scale graphic elements.
Using GlobeGraphics API for custom drawings
It is possible to some degree to manage dynamic content using GlobeGraphicsLayer elements. ArcGlobe supports an API that is very similar to the existing API of ArcMap/MapControl for managing graphic elements. You can use different types of graphic elements with quite a complex symbology such as 3D marker symbols, text symbols, 3D character marker symbols, and so on.
 
Globe supports the addition of numerous graphic layers that allow you to manage your dynamic elements in a dedicated layer and control whether to display or hide the layer. This serializes the dynamic content and prevents you from having to understand the globe drawing to manage graphic elements in a graphic layer. It is very likely that code written to work in ArcMap or ArcScene can be easily modified with minor changes and work inside a Globe application.
 
Though very easy to apply, using GlobeGraphicsLayer graphic elements to symbolize dynamic objects might not be the best solution in terms of performance and design. Graphic elements have quite an overhead because each graphic element needs to have its own symbol and geometry. These graphic elements need to be very flexible in terms of usability and thus support different categories of element types, symbology, and geometries. For that reason, these objects consume a large amount of resources, especially if used dynamically.
 
The following code demonstrates how to create a new graphic layer and add it to the globe:

[C#]
//Declaration of a graphic layer.
private IGlobeGraphicsLayer m_globeGraphicsLayer  = null;

//Create the graphic layer.
m_globeGraphicsLayer = new GlobeGraphicsLayerClass();
((ILayer)m_globeGraphicsLayer).Name = "DynamicObjects";

IScene scene = (IScene)m_globeDisplay.Globe;

//Add the new graphic layer to the globe.
scene.AddLayer((ILayer)m_globeGraphicsLayer, false);

//Activate the graphic layer.
scene.ActiveGraphicsLayer = (ILayer)m_globeGraphicsLayer;
The following code demostrates how to create a new graphic element and add it to the graphic layer:
 

[C#]
//Create the element’s geometry.
IPoint point = new PointClass();
(IZAware(point)).ZAware = true;
point.PutCoords(position.longitude, position.latitude);
point.Z = position.altitude;

//Create the element’s color (red).
IRgbColor color = new RgbColorClass();
color.Red = 255;
color.Green = 0;
color.Blue = 0;

//Create the element’s symbol.
IMarkerSymbol markerSymbol = new SimpleMarker3DSymbolClass();
//Set the marker symbol's style and resolution.
((ISimpleMarker3DSymbol)markerSymbol).Style = esriSimple3DMarkerStyle.esriS3DMSSphere;
((ISimpleMarker3DSymbol)markerSymbol).ResolutionQuality = 1.0;
markerSymbol.Size = 700;
markerSymbol.Color = color as IColor;

//Create the new marker symbol’s element.
IElement trackElement = new MarkerElementClass();

//Set the element's symbol and geometry (location and shape).
((IMarkerElement)trackElement).Symbol = markerSymbol;
trackElement.Geometry = point as IPoint;

//Add the element to the graphic layer.
((IGraphicsContainer)globeGraphicsLayer).AddElement(trackElement, 0);
The next code example demonstrates how to update a graphic element inside a graphic layer:

[C#]
//Get the element by its index.
IElement elem = ((IGraphicsContainer3D)m_globeGraphicsLayer).get_Element(m_trackObjectIndex);

//Get the geometry of the element.
IPoint point = elem.Geometry as IPoint;

//Update the element's position.
point.PutCorrds(position.longitude, position.latitude);
point.Z = position.altitude; 
elem.Geometry = (IGeometry)point;

//Update the element in the graphic layer.
m_globeGraphicsLayer.UpdateElementByIndex(m_trackObjectIndex)

 

Advanced drawing

 
Advanced drawing can be done through programming. In ArcGIS 9.2, the GlobeCore API has been significantly improved to facilitate the advanced 3D static and dynamic drawing.
 
Methods of implementation
There are two main methods to add user-defined drawings to the globe: drawing within the content of a command item and custom drawing as part of a custom layer implementation.
 
Drawings as part of the implementation of a command or tool is normally used to draw mouse feedback and items that don’t require management. You should implement a custom globe layer if you want the client of your application to set the visibility of dynamic data, control the minimum and maximum scale in which the items are visible, identify dynamic objects, and so on.
 
Drawing from tools and commands
 
Starting in 9.2, it is possible to use a direct OpenGL plug-in to draw a user-defined object on the globe’s display. Developers have the freedom to use an OpenGL API to draw to the globe’s display or ArcObjects to call Display::SetSymbol followed by Display::DrawXXX, which is similar to the custom drawing available in MapControl and ArcMap. The drawing can take place as part of the implementation of a command or tool using the following sequence:
 
IGlobeDisplay3.DirectOpenGLDraw = true;
IDisplay.SetSymbol();
IDisplay.DrawXXX();
IGlobeDisplay3.DirectOpenGLDraw = false;
 
Another option is through listening to GlobeDisplayEvents and implementing the drawing inside IGlobeDisplayEvents::BeforeDraw or IGlobeDisplayEvents::AfterDraw. This technique can be used to draw mouse feedback on the globe, edit your objects in the globe, or for any other purpose.
 

Implementing custom globe layers

 
The ArcGIS libraries define many different types of layer classes to visually represent different sources of data (for example, FeatureLayer, RasterLayer, CadLayer, and AnnotationLayer). These layers display geographic data stored in datasets, such as shapefiles, CAD files, image files, and feature classes, which are stored in a geodatabase.
 
You may have a custom or unsupported data format that you want to display in a globe without having to first convert it to a data format supported by ArcGIS. In many cases, you will have to connect and display real-time data that comes from a live feed making any connection to a standard database irrelevant. Also, writing custom layers allows you to change the way an existing layer’s class draws.
 
There are three main types of custom layers that can be implemented by a class that implements ICustomGlobeLayer:
 
 
Considerations for implementing a globe custom layer
 
There are many ways to implement a custom layer. Before implementing your layer, review the following issues that may affect the architecture of your layer:
 
 

Preparing the application to draw 3D graphics on a globe

 
Globe’s drawing pipeline is different and more sophisticated then typical 2D graphic drawings on ArcMap or a map control display. The GlobeViewer object that is associated with OpenGL Rendering Context is passed as the parameter of type ISceneViewer in the BeforeDraw() and AfterDraw() method of the IGlobeDisplayEvents interface. These two methods are the place where OpenGL functions are safe to use. The AfterDraw() method is the recommended one to use.
 

Broadcasting activities through IGlobeDisplayEvents

 
The GlobeDisplay broadcasts some of its activities through IGlobeDisplayEvents. The AfterDraw() event is broadcasted right after the drawing activity is done. The application can listen to these events by implementing IGlobeDisplayEvents on a class (e.g., a custom layer, a tool’s coclass).
 
The following code shows how the AfterDraw event is wired:

[C#]
//Start listening to globe display events.
((IGlobeDisplayEvents_Event)m_globeDisplay).AfterDraw += new 
IGlobeDisplayEvents_AfterDrawEventHandler(OnAfterDraw);
Notice the inline casting of IGlobeDisplayEvents and the usage of IGlobeDisplayEvents_Event. In ArcObjects.Net, the event interfaces all have an _Event suffix, because event interfaces (also known as outbound interfaces) are automatically suffixed with _Event by the type library importer.
 
Upon broadcasting the event, the GlobeDisplay also receives the response from the listener by executing the broadcasted method. This is a two-way communication between the broadcaster and the receiver. Any code implemented inside of the BeforeDraw() and AfterDraw() method will be executed by the GlobeDisplay.
 
The following shows how to draw mouse feedback inIglobeDisplayEvents::AfterDraw:

[C#]
private void OnAfterDraw(ISceneViewer pViewer)
{  
  //Test whether in drawing mode. 
  if(false == m_bDrawPoint)
    return;

  //Convert the mouse coordinate into a geocentric (OpenGL) coordinate system.
  double glX, glY, glZ;
  m_globeViewUtil.WindowToGeocentric(m_globeDisplay, m_sceneViewer, 
                                     m_srcX, m_srcY, true,
                                     out glX, out glY, out glZ);

  //Draw the converted point on the surface of the globe.
  GL.glPointSize(15.0f);
  GL.glColor3ub(255, 0, 0);
  GL.glBegin(GL.GL_POINTS);
     GL.glVertex3f((float)glX, (float)glY, (float)glZ);
  GL.glEnd();

 

Tuning in to communicate with the GlobeDisplay

 
In a case where you are implementing a command or a tool that requires you to listen to GlobeDisplayEvents, you can use the GlobeHookHelper class, introduced in ArcGIS 9.2. Writing code that uses GlobeHookHelper allows you to write commands and tools that can be shared between ArcGlobe and GlobeControl applications. Through the IGlobeHookHelper interface, you get access to the GlobeDisplay, Globe, Camera, and the ActiveViewer.
 
In ArcGIS 9.2, the 3D drawing pipeline has been simplified, and the GlobeGraphicsLayer has been introduced, as well as the ICustomGlobeLayer and other utilities, such as IGlobeViewUtil, which allows you to convert coordinates for each of the coordinate systems used by Globe.
 

Implementing a 3D Custom Layer

 
The GlobeCore API provides the ICustomGlobeLayer interface for more advanced 3D graphic drawings. Any layer that will be used to perform 3D drawings on the globe display needs to support this interface. This interface simplifies the overhead of writing custom layers for ArcGlobe/GlobeControl. Developers do not have to listen to GlobeDisplayEvents to do their drawings. Instead, ICustomGlobeLayer provides a method (DrawImmediate) where all drawings should take place.
 
The following shows wiring AfterDraw inside the ICommand.Onclick event handler using GlobeHookHelper.

[C#]
public override void OnClick()
{
m_globeDisplay = (IGlobeDisplay3)m_globeHookHelper.GlobeDisplay;
//Start listening to globe display events.
((IGlobeDisplayEvents_Event)m_globeDisplay).AfterDraw += new IGlobeDisplayEvents_AfterDrawEventHandler(OnAfterDraw);
}

/// <summary>
/// GlobeDisplay's AfterDraw event handler.
/// </summary>
/// <param name="pViewer"></param>
private void OnAfterDraw(ISceneViewer pViewer)
{
 //AfterDraw event handler logic goes here.
}
 
The important methods developers should pay attention to when implementing a custom layer are DrawType() and DrawImmediate(). The Hit() method should also be implemented to support identify, picking, and selection functions. Details and sample code for each method will be discussed later in this document.
 
In most cases, you will implement a custom layer to draw custom graphics. For that reason, your class DrawType() method must return esriGlobeCustomDrawOpenGLto specify to the globe framework that the layer uses custom drawing. In addition to ICustomGlobeLayer, your class must also implement the rest of the interfaces that are required to implement a layer for ArcGIS:
 
 
The .NET SDK for ArcGIS 9.2 includes an abstract BaseClass that implements a CustomGlobeLayer in addition to a CustomGlobeLayer template that overrides the relevant methods of the BaseClass and allows rapid development. For more information, please refer to Additional Resources for .NET Developers.
 
Setting up to draw OpenGL
 
To draw inside the globe display, you must set the symbology for your drawing and be able to transform your data from its underlying coordinate system into globe’s geocentric coordinate system, which is the coordinate system used by OpenGL.
 
You have the option of using ArcObjects to load standard symbology such as 3D models or use OpenGL directly to create a user-defined symbology, such as billboard images, spheres, or any other symbol type. Currently, there is no managed OpenGL library that is part of the .NET framework (although it is planned for future operating systems). For that reason, you need to use an Open Source managed wrapper library for OpenGL or directly import the relevant methods into your assembly.
 
Loading a 3D model from file
 
Graphical 3D models that are available in OpenFlight (.flt) or 3D Studio (.3ds) format can be used as Marker3DSymbol. The IMarker3DSymbol interface provides the method CreateFromFile() to load the 3D model, for example, buildings, trees, and vehicles into memory.
 
The following code shows loading a 3D model from file:
 

[C#]
//Loading 3D model from file.
IMarker3DSymbol marker3DSymbol = new Marker3DSymbolClass();
marker3DSymbol.CreateFromFile(path);
 
Alternatively, you can also use simple 3D marker symbols, such as diamonds and spheres. See the following code example that shows how to use simple 3D marker symbols:
 

[C#]
//Create a new 3D marker symbol.
IMarkerSymbol markerSymbol = new SimpleMarker3DSymbolClass();

//Set the marker symbol's style and resolution.
((ISimpleMarker3DSymbol)markerSymbol).Style = esriSimple3DMarkerStyle.esriS3DMSSphere;
((ISimpleMarker3DSymbol)markerSymbol).ResolutionQuality = 1.0;

//Set the symbol's size and color.
markerSymbol.Size = 700;
markerSymbol.Color = color as IColor;
Creating display lists from 3D symbols
 
Performance plays an important role for 3D real-time applications. Encapsulating symbology inside a display list allows you to group together and cache a set of OpenGL commands with the ArcObjects symbology. This causes the commands within the list to be executed as if they were given normally.
 
When creating the display list, you should scale the symbol so that it will appear as its actual size inside the globe display or set the symbol’s size to one unit, meaning that you will have to scale it each time before drawing it.
 
Calculating the symbol size is done according to the globe’s radius. The globe’s radius is given in meters where the sphere’s radius, which represents the globe in the geocentric coordinate system, is normalized to one unit. Therefore, scaling your symbol to 1/Globeradius gives you its actual size when drawn in the globe display.
 
See the following code that shows how to calculate the symbol scale:
 

[C#]
private uint CreateDisplayList(IMarker3DSymbol marker3DSymbol)
{
   m_globeDisplay.DirectOpenGLDraw = true;
   GL.glMatrixMode(GL.GL_MODELVIEW);

   IDisplay display = (IDisplay)m_globeDisplay;
   IGlobeDisplayRendering globeDisplayRendering = (IGlobeDisplayRendering)m_globeDisplay;
   double globeRadiusMeters = globeDisplayRendering.GlobeRadius;

   //Calculate the symbol scale.
   double scale = 1.0 / globeRadiusMeters; //Normalized by the Globe Radius.

   uint intSymbolDisplayList = GL.glGenLists(1);
   GL.glNewList(intSymbolDisplayList, GL.GL_COMPILE);
   {
     GL.glDisable(GL.GL_COLOR_MATERIAL);
     GL.glPushMatrix();
     {
       GL.glScaled(scale, scale, scale);
       display.SetSymbol((ISymbol)marker3DSymbol);
     }
     GL.glPopMatrix();
     GL.glEnable(GL.GL_COLOR_MATERIAL);
   }
   GL.glEndList();

    m_globeDisplay.DirectOpenGLDraw = false;

    return intSymbolDisplayList;
}
 
After caching your symbol inside a display list, the drawing becomes a sequence of translating, rotating, scaling, and calling your display lists.
 
Coordinating systems for drawing 3D graphics
 
To draw 3D graphics using the OpenGL API, you need to convert all coordinates to the geocentric rectangular coordinate system. This means that the origin of the coordinate is at the center of the globe. The x,y plane coincides with the equatorial plane, and the z-axis points to the north pole. The factors that influence the coordinate values are the measurement unit (e.g., from feet to meters), the globe radius, and the height exaggeration factor.
  
Globe coordinate system
 
The globe is represented by a unit sphere where the globe radius is normalized to one unit. Therefore, the normalized globe radius dictates the scale between the real world object and the geocentric coordinate system.
 
See the following example that shows how to calculate the scale for a globe coordinate system:
 

[C#]
IGlobeDisplayRendering globeDisplayRendering = (IGlobeDisplayRendering)m_globeDisplay;
double globeRadiusMeters = globeDisplayRendering.GlobeRadius;
double scale = 1.0 / globeRadiusMeters; //Normalized by the Globe Radius.
 
The following illustration shows a globe spherical-based geocentric coordinate system (normalized with the globe radius):
Globe spherical-based geocentric coordinate system (normalized with the globe radius). 
In ArcGIS 9.2, a new utility interface has been introduced that allows you to convert coordinates for each of the coordinate systems used by globe.
 
The following code shows how to convert coordinates for each coordinate system using globe:
 

[C#]
//Declaring the GlobeViewUtil.
private IGlobeViewUtil m_globeViewUtil = null;

//Cast GlobeViewUtil from the globe camera.
m_globeViewUtil = sceneViewer.Camera as IGlobeViewUtil;

//Convert a geographic coordinate to geocentric.
m_ipGlobeViewUtil.GeographicToGeocentric(longitude, latitude, altitudeMeters, 
out X, out Y, out Z);

//Convert geocentric to geographic.
m_ipGlobeViewUtil.GeocentricToGeographic(X, Y, Z, 
out longitude, out latitude, out altitudeMeters);

//Convert geocentric to window.
int winX, winY;
m_globeViewUtil.GeocentricToWindow(x, y, z, out winX, out winY);

//Convert window to geographic.
m_globeViewUtil.WindowToGeographic(m_globeDisplay, m_sceneViewer, X, Y, true, 
out lon, out lat, out alt);

 

Local object’s coordinate system
 
To standardize the model’s transformation, it must be assumed that a local object’s coordinate system is built where the z-axis (z-height) points up, the y-axis (y-north) goes from the front of the model to the back, and the x-axis (x-east) completes a right-handed system. In cases where your model’s local coordinate system is built differently, you will have to apply additional rotations to align your model.
 
See the following illustration that shows a model’s local coordinate system:
 
 
 
Coordinate transformation
 
All objects need to be transformed into the range of -1 to 1 of the x, y, and z-axes. Since the origin of the geocentric coordinate system is in the center of the globe, all translations must be done accordingly. Therefore, to draw an object in the lat, lon, and alt coordinates, you must first convert the geographical coordinate into the geocentric coordinate system, then translate the OpenGL matrix to that location (by calling glTranslate).
 
Translation alone is not enough, since the object must be oriented in the right direction. By default, drawing the object without orienting it will end up with the object getting drawn aligned with the geocentric coordinate system. This means that the object gets drawn parallel to the x,y plane. Normally, the object’s up direction must be kept pointing outward from the globe and perpendicular to the globe’s surface. This direction must be aligned with the normal vector of the sphere representing the globe. In addition, each 3D model is built differently with a different local coordinate system.
 
Aligning the object’s up direction
 
The following code aligns the object’s up direction to the globe’s normal vector. The code first calculates the rotation that must be applied to orient the model, then applies the translation and orientation on the object.
 

[C#]
private void CalculateSymbolOrientation(ISceneViewer sceneViewer, double glX, double glY, 
double glZ, out WKSPointZ orientation)
{
  m_vector3D.SetComponents(glX, glY, glZ);
  double inclination = m_vector3D.Inclination;
  double azimuth = m_vector3D.Azimuth;

  orientation.X = (-1.0 * inclination*(180.0/Math.PI)) + 180.0;
  orientation.Y = 0.0;
  orientation.Z = 180.0 - azimuth*(180.0/Math.PI);
}

void TranslateAndRotate(double glX, double glY, double glZ, WKSPointZ symbolOrientation, 
double udfAzimuth)
{
  GL.glTranslated(glX, glY, glZ);
  GL.glRotated(symbolOrientation.Z, 0.0, 0.0, 1.0);
  GL.glRotated(symbolOrientation.X, 1.0, 0.0, 0.0);
  GL.glRotated(symbolOrientation.Y, 0.0, 1.0, 0.0);
  GL.glRotated(udfAzimuth, 0.0, 1.0, 0.0);
}
 
The following illustrates how the adjustment makes the objects display right-side up. Without proper orientation, objects can appear upside down in some locations.
 
Aligning objects 
 

The drawing sequence

 
Drawing takes place when the GlobeDislay redraws. You should plan your custom layer so that it will redraw the globe display in a place that reflects the changes of the layer’s content.
 
When implementing a custom layer, all drawing should take place inside the method ICustomGlobeLayer.DrawImmediate. Apart from the GlobeDisplay’s BeforeDraw and AfterDraw; this is the only place where OpenGL is ready to execute the user’s commands. For that reason, all initializations, such as building display lists and mapping textures, as well as the drawing should take place in the DrawImmediate call. The basic drawing sequence should have the following steps:
 
  1. Call glPushMatrix to push the current matrix stack. This needs to be done since you are about to manipulate the transformation stack to put the object in place. 
  2. Normally, you have to convert your object’s coordinates from real-world geographic coordinates into a geocentric coordinate system. For this reason, you have to reproject (if required) your object’s coordinate into a WGS1984 geographical coordinate system.
  3. Once you have your object’s coordinate in a WGS1984 geographical coordinate system, you need to convert it into a geocentric coordinate system. You can use the IGlobeViewUtil interface for that purpose.
  4. Calculate the object’s orientation so that it will be parallel to the globe and aligned with the object’s heading direction. This means aligning the Z-axis to the globe’s radius vector.
  5. Translate the object into place and if necessary, orient the object.
  6. Scale the object to display in its real size or if necessary, aggregate the object’s size.
  7. Call the display list of the relevant symbol in which to draw.
  8. Call glPopMatrix to pop back the current matrix stack.
 
The following code sample shows a very simple implementation of the DrawImmediate call:
 

[C#]
public override void DrawImmediate(IGlobeViewer pGlobeViewer)
{   
   //Ensure the layer is visible and valid.
   if (!m_bVisible || !m_bValid)
      return;

   //Only once, create display lists and do other initializations.
   if (!m_bDisplayListCreated)
   {
      CreateDisplayLists();
   }

   //Get the GlobeDisplay.
   IGlobeDisplay globeDisplay = pGlobeViewer.GlobeDisplay;

   //Get the active viewer. 
   SceneViewer sceneViewer = globeDisplay.ActiveViewer;

   //Calculate the scale.
   IGlobeDisplayRendering globeDisplayRendering = IGlobeDisplayRendering)m_globeDisplay;
   double globeRadiusMeters = globeDisplayRendering.GlobeRadius;
   double scale = 1.0 / globeRadiusMeters;

   IGlobeDisplayRenderingPtr ipGlobeDisplayRend(pGlobeDisplay);

     double glX, glY, glZ;
   WKSPointZ wksSymbolOrientation;

   . . .

     //Translate the object into place.
   GL.glPushMatrix();
   {
     //Get the object’s OpenGL coordinate. 
     m_ipGlobeViewUtil.GeographicToGeocentric(obj.dLongitude, 
                                               obj.dLatitude, 
                                               obj.dAltitudeMeters, 
                                               out glX, out glY, out glZ);
     //Calculate the object’s orientation.
     CalculateSymbolOrientation(ipSceneViewer, glX, glY, glZ, 
                                wksSymbolOrientation);  

     //Translate the object into place and orient it.
     TranslateAndRotate(glX, glY, glZ, wksSymbolOrientation ,dblSymbolAngle); 

     //Set the object’s scale.
     GL.glScaled(dScale,dScale,dScale);

     //Draw the object.
     GL.glCallList(m_intMainSymbolDisplayList);       
   }
   GL.glPopMatrix();  
}
 
Optimizing the drawing sequence
 
Implementation of custom layers for ArcGlobe/GlobeControl would probably be done for real-time applications. Usually, such applications are required to match a very high standard of performance, whether it is the ability to display thousands of elements that pertain to one data feed or update objects in a very high frequency.
 
Although efficient, caching your symbol inside a display list is not always enough to get the best performance. The following are the major techniques to optimize the drawing sequence:
 

 

Minimizing the computation load inside DrawImmediate
 
In most cases, the layer’s drawing frequency does not match the object’s update rate. An object is usually subjected to update every several drawing cycles. For that reason, calculating the object’s geocentric coordinate and orientation only when the element gets updated significantly decreases the computations that must be done on each drawing cycle.
 
To draw the object, you will have to cache its calculated geocentric coordinate and orientation so it can be used inside the DrawImmediate method.
 
The following code shows how to minimize the computation load:
 

[C#]
public bool AddItem(long zipCode, double lat, double lon) 
{
 double X = 0.0, Y = 0.0, Z = 0.0;    
 . . .
 DataRow r = m_table.Rows.Find(zipCode);
 if (null != r) //If the record with this ZIP Code already exists.
 {
   //Cache the item’s geocentric coordinate to save the calculation inside method 
DrawImmediate ().
   globeViewUtil.GeographicToGeocentric(lon, lat, 1000.0, out X, out Y, out Z);

   //Update the record.
   r[3] = lat;
   r[4] = lon;
   r[5] = X;
   r[6] = Y;
   r[7] = Z;

   lock (m_table)
   {
      r.AcceptChanges();
   }
 }
. . .
}
 
Scaling and aggregating symbols
 
Usually, one of the first techniques learned in a geographic information system (GIS) class for drawing spatially enabled data is to use aggregated symbology (as scale ratio becomes small when zooming out). 3D drawing is no exception. However, in a 3D environment, the scale constantly varies since there is no reference surface that can calculate a scale accordingly. Therefore, the distance from the globe’s camera of each object determines a reference scale. The distance from the object to the camera can be calculated both in geographical units and in geocentric units.
 
The following shows calculating the scale (magnitude) according to the distance from the camera to the object using geocentric units and taking into consideration the elevation exaggeration:
 

[C#]
double dblObsX, dblObsY, dblObsZ, dMagnitude;
ICamera camera = pGlobeViewer.GlobeDisplay.ActiveViewer.Camera;

//Get the camera location in a geocentric coordinate (OpenGL coordsystem).
camera.Observer.QueryCoords(out dblObsX, out dblObsY);
dblObsZ = camera.Observer.Z;
      
IGlobeDisplayRendering globeDisplayRendering = IGlobeDisplayRendering)m_globeDisplay;
double baseExaggeration = globeDisplayRendering.BaseExaggeration;

double X = 0.0, Y = 0.0, Z = 0.0;
X = Convert.ToDouble(rec[5]);
Y = Convert.ToDouble(rec[6]);
Z = Convert.ToDouble(rec[7]);

m_ipVector3D.SetComponents(dblObsX - X, 
                           dblObsY - Y, 
                           dblObsZ - Z);

dMagnitude = m_ipVector3D.Magnitude;

double scale = dblMagnitude * baseExagFactor;
 
Setting the scale threshold, which determines whether to draw a full symbol or an aggregated symbol is up to the developer, usually according to a requirement defined as a geographical distance. In many cases, setting the threshold is done empirically to get the best balance between the aggregated symbols and the full symbol that looks best and gives the best performance. The aggregated symbol can be any type of symbol, cached as a display list or a simple OpenGL geometry. There is no limit to the number of aggregated symbol levels that can be set to an object.
 
The following code shows how to set the scale threshold:
 

[C#]
//If far away from the object, draw it as a simple OpenGL point.
if(scale > 2.5)
{
   GL.glPointSize(5.0f);
   GL.glColor3ub(0, 255, 0);
   GL.glBegin(GL.GL_POINTS);
      GL.glVertex3f(Convert.ToSingle(X), Convert.ToSingle(Y), Convert.ToSingle(Z));
   GL.glEnd();

}
else //When close enough, draw the full symbol.
{
  GL.glPushMatrix();
  {
     //Translate and orient the object into place.
     TranslateAndRotate(X,Y, Z, wksSymbolOrientation, dSymbolAngle);

     //Scale the object.
     GL.glScaled(dObjScale, dObjScale, dObjScale); 
      
     //Draw the object.       
     GL.glCallList(m_intMainSymbolDisplayList);
  }
  GL.glPopMatrix();  
}
 
Screening out objects outside the display viewport
 
Drawing dynamic objects usually involves a large amount of computation. In addition, drawing a complex full-resolution model that contains a large amount of triangles can take a considerable amount of resources. The camera’s field of view is limited to a certain degree. Therefore, many objects get drawn despite the fact they are not visible inside the viewport. Although OpenGL will not draw these objects, the computation required to draw an object might be quite expensive. For that reason, screening out objects outside the viewport is a good practice to preserve resources and make your code more efficient.
 
To screen out objects, you need to convert the object’s coordinate into the window’s coordinate and test whether it is inside the viewport. There are two ways to convert a coordinate into a window coordinate. You can use the IGlobeViewUtil interface to convert a coordinate from each of the other coordinate systems used by globe (geographic or geocentric) into the window system or you can use OpenGL to convert from a geocentric coordinate into a window coordinate.
 
Calling OpenGL to project a coordinate from geocentric into a window coordinate system requires you to get from OpenGL the projection matrix, model matrix, and the viewport matrix. This can only be done inside the DrawImmediate method (in the case of a globe’s custom layer) or inside IGlobeDisplayEvents::BeforeDraw and IGlobeDisplayEvents::AfterDraw.
Although, using OpenGL to convert from geocentric coordinates into window coordinates is very fast and accurate (it also allows you to screen out objects outside the clipping planes), when switched into selection mode, the projection matrix changes and results in erroneous calculations.
 
For that reason, it is possible to use a mixed model of ArcObjects together with OpenGL to test if an object is inside the viewport. When OpenGL is in selection mode, use IGlobeViewUtil to convert into a window coordinate and the rest of the time, use OpenGL.
 
The following code shows testing an object inside the viewport:
 

[C#]
private bool InsideViewport(double x, double y, double z, double clipNear, uint mode)
{

bool inside = true;

   //In selection mode, the projection matrix is changed.
   //Therefore, use the GlobeViewUtil because calling gluProject gives unexpected results.
   if (GL.GL_SELECT == mode)
   {
     int winX, winY;
     m_globeViewUtil.GeocentricToWindow(x, y, z, out winX, out winY);
     
     inside = (winX >= m_viewport[0] && winX <= m_viewport[2]) && 
              (winY >= m_viewport[1] && winY <= m_viewport[3]);

   }
   else
   {
        
      //Use gluProject to convert into the window’s coordinate.
      
unsafe
      {
         double winx, winy, winz;
         GLU.gluProject(x, y, z, 
                        m_modelViewMatrix, 
                        m_projMatrix, 
                        m_viewport, 
                        &winx, &winy, &winz);

         inside = (winx >= m_viewport[0] && winx <= m_viewport[2]) &&
                  (winy >= m_viewport[1] && winy <= m_viewport[3] &&
                  (winz >= clipNear && winz <= 1.0));
        }        
   }

   return inside;
}
 
The following code filters out objects outside of the viewport:

[C#]
//Get the OpenGL matrices.
GL.glGetDoublev(GL.GL_MODELVIEW_MATRIX, m_modelViewMatrix);
GL.glGetIntegerv(GL.GL_VIEWPORT, m_viewport);
GL.glGetDoublev(GL.GL_PROJECTION_MATRIX, m_projMatrix);

//Get the OpenGL rendering mode.
uint mode;
unsafe
{
   int m;
   GL.glGetIntegerv(GL.GL_RENDER_MODE, &m);
   mode = (uint)m;
}

//Get the globe’s near clipping plane. The ClipNear value is required for the viewport filtering 
(since we don’t want to draw an item beyond the clipping planes).
IGlobeAdvancedOptions advOpt = m_globeDisplay.AdvancedOptions;

double clipNear = advOpt.ClipNear;

//Ensure the object is within the viewport.
if (!InsideViewport(X, Y, Z, clipNear, mode))
   continue;
//Continue with the drawings here.
. . .
 
Using a single timer to redraw the display
 
Since the data managed by a custom layer or in custom drawings is usually dynamic, you need to constantly redraw the globe display to reflect changes to that data. The redraw frequency should be determined by the amount of time it takes each object to update, as well as the amount of objects that you have to address in the application.
 
Since Window’s operating system cannot support a true real-time application, it is usually acceptable to have a delay of a few milliseconds between the time that an object gets updated and the time that it gets drawn in the globe display. Therefore, having a timer in your application that constantly redraws the display is usually the best solution when your data is dynamic.
 
The timer's elapsed event is raised on a ThreadPool’s thread. This means it is executed on another thread that is not the main thread. The solution is to treat an ArcObjects component like a user interface (UI) control by using the Invoke method to delegate the call to the main thread (where the ArcObjects component was created) and prevent making cross apartment calls.
 
To support the Invoke method, your class needs to implement the ISynchronizeInvoke .NET interface. Alternatively, your class can inherit from the System.Windows.Forms.Control .NET interface. This way, your class automatically supports the Invoke method.
 
The following shows a class inheriting the System.Windows.Forms.Control interface:
 

[C#]
//Class definition.
public abstract class GlobeCustomLayerBase : Control,
                                             ILayer,
                                             IGeoDataset,
                                             . . .
   //Class constructor.
   public GlobeCustomLayerBase()
   {  
      //Call CreateControl to create the handle.
      this.CreateControl();
   }

. . .
 
The following code shows using the timer to redraw the GlobeDisplay:
 

[C#]
//Redraw delegate invokes the timer's event handler on the main thread.
private delegate void RedrawEventHandler();

//Set the layer’s redraw timer.
private System.Timers.Timer m_redrawTimer = null;

//Initialize the redraw timer.
m_redrawTimer = new System.Timers.Timer(200);
m_redrawTimer.Enabled = false;
//Wire the timer’s elapsed event handler.
m_redrawTimer.Elapsed += new ElapsedEventHandler(OnRedrawUpdateTimer);

//Redraw the timer’s elapsed event handler.
private void OnRedrawUpdateTimer(object sender, ElapsedEventArgs e)
{
   //Since this is the timer’s event handler, it gets executed on a different thread than the 
main one. Therefore, the Invoke call is required to force the call on the main thread 
and prevent cross apartment calls.
   if(m_bTimerIsRunning)
      base.Invoke(new RedrawEventHandler(OnRedrawEvent));  
}

//This method invoked by the timer’s elapsed event handler and gets executed on the 
main thread.
void OnRedrawEvent()
{
   m_globeDisplay.RefreshViewers();
}
 
Another alternative is to redraw the globe display each time an element gets updated. However, this approach can lead to a nonstop redraw of the display when there are a large number of objects and could consume all of the system resources.
 
When there is more than one dynamic layer, having multiple timers for each layer can result in a constructive interference of the timers, which may lead to excessive uncontrolled redrawing of the globe display. For that reason, the best solution is to implement a common singleton object that serves all layers that require constant drawing. This way, no matter how many dynamic layers get added to the globe, the drawing rate remains constant.
 
The following C++ active template library (ATL) class declaration code shows how the global timer singleton is declared:
 
 
 
The following code to use the global timer class is straightforward:
 

[C#]
//Declare the global timer.
private IGlobalTimer m_globalTimer = null;

public void DrawImmediate(IGlobeViewer pGlobeViewer)
{
   if(!m_bVisible || !m_bValid)
      return;
   . . .

   if(null == m_globalTimer)
   {
      m_globalTimer = new GlobalTimerClass();
      m_globalTimer.GlobeDisplay = pGlobeViewer.GlobeDisplay;
      if(m_globalTimer.RedrawInterval > 200)
         m_globalTimer.RedrawInterval = 200;
      m_globalTimer.Start();
   }
   . . .

 

Managing dynamic objects

 
There is a great deal of importance to the underlying data structure used to manage dynamic data. Dynamic data may be streaming in from an external feed, synchronically or asynchronously. As a developer, you may be required to support different types of functionality such as selection, identifying, HitTest, and dynamic selection. In most cases, your data structure will have to support sequential access, which is required to draw the layer, as well as random access to implement selection, identify, flash, and so on.
 
In many cases, you will have to connect to an external, real-time feed that is running on a different thread in your class. Therefore, the selected data structure will have to be thread-safe. Since memory consumption and performance are two of the most important aspects of this type of layer, choosing an efficient and fast data structure is a must. The selection of the data structure can vary according to the development environment in which you are implementing the layer. Depending on your requirements, you might consider using ADO.Net DataTable, or a HashTable, or even choose to implement your layer using a generics collection.
 
The following code shows a user-defined data structure that is storing real-time data using ADO.Net DataTable:
 

[C#]
private DataTable m_table = null;

public GlobeCustomLayerBase()
{
  m_table = new DataTable("RECORDS");

  //Add columns to the table.
  m_table.Columns.Add("ID",       typeof(long));
  m_table.Columns.Add("ZIPCODE",  typeof(long));
  m_table.Columns.Add("CITYNAME", typeof(string));
  m_table.Columns.Add("LAT",      typeof(double));
  m_table.Columns.Add("LON",      typeof(double));
  m_table.Columns.Add("X",        typeof(double));
  m_table.Columns.Add("Y",        typeof(double));
  m_table.Columns.Add("Z",        typeof(double)); 
   . . .
  
   //Set the columns’ properties.
  m_table.Columns[0].AutoIncrement = true;
  m_table.Columns[0].ReadOnly = true; 
   
  //Set the ZIP Code primary key for the table.
  m_table.PrimaryKey = new DataColumn[] { m_table.Columns["ZIPCODE"] };
  . . .
 
The following code shows accessing the data stored in the DataTable:
 

[C#]
//Sequential access.
foreach (DataRow rec in m_table.Rows)
{
   lat  =  Convert.ToDouble(rec[3]);
   lon  =  Convert.ToDouble(rec[4]);
   X    =  Convert.ToDouble(rec[5]);
   Y    =  Convert.ToDouble(rec[6]);
   Z    =  Convert.ToDouble(rec[7]);
   . . .
}

//Random access, first make sure the item exists. 
//To call the Find()method, set columns as a primary key. 
DataRow r = m_locations.Rows.Find(zip);
if (null == r)

 . . .
 
The following code shows acquiring exclusive lock to support thread safety:
 

[C#]
//Add the current location to the location table.
DataRow r = m_locations.Rows.Find(zip);
if (null == r)
{
   r = m_locations.NewRow();
   r[1] = zip;
   r[2] = cityName;

   //Lock the location table to prevent access from other
   threads while adding the new record to the table. 
   lock (m_locations)
   {
      m_locations.Rows.Add(r);
   }
}

 

Connecting to real-time feeds

 
There is variety of real-time feeds you may have to use in your custom layer. You may be required to listen on a communication port that receives Transmission Control Protocol/Internet Protocol (TCP/IP) or User Datagram Protocol/Internet Protocol (UDP/IP) packets, connect to an ArcGIS Tracking Server, or use a Web service and communicate through a Simple Object Access Protocol (SOAP). Each of the real-time feed types requires a different connection approach. Getting data from the server is done with two methods: push and pull mode.
 
 
Optimizing performance, especially when connecting to a Web service after getting a response from the server, may take a long time. Moving the connection to a background thread may be more logical. Make sure to separate the real-time feed connection from the drawing sequence as much as possible to prevent it from affecting the drawing performance.
 
The following code snippet demonstrates pulling messages from an ArcGIS Tracking Server feed:
 

[C#]
private System.Timers.Timer m_timer = null;

//Declare the invoke delegate.
private delegate void MessageHandler(IMessage msg);

//Set the update timer interval to 10MS.
m_timer = new System.Timers.Timer(10);
m_timer.Enabled = false;

//Wire the timer’s elapsed event.
m_timer.Elapsed += new ElapsedEventHandler(OnTimer);

//Pull the messages from the server.
private void OnTimer(object sender, ElapsedEventArgs e)
{
   try
   { 
      IMessage msg = m_internetServerConnection.getMessage(0);
      if(msg == null)
         break;

//Process the incoming message.
      this.Invoke(new MessageHandler(HandleMessage), new object[] { msg });
    }
    catch(Exception ex)
    {
       System.Diagnostics.Trace.WriteLine(ex.Message);
    }
}


//Handle the incoming message.
private void HandleMessage(IMessage msg)
{
   IDataMessage dataMessage = (IDataMessage)msg;
   int flightId = Convert.ToInt32(dataMessage.getColumn(1));
   . . . 
}

 

Serializing the custom layer

 
Supporting layer serialization is important due to the multithread nature of the ArcGlobe application. Typically, the custom layer is created on the main thread and other methods are called from a different thread. Serialization is used to prevent marshalling of very large objects across thread apartments, especially when the operation is taking a considerable amount of time. To prevent this, ArcGlobe tries to clone the instance of the custom layer using serialization. Cloning the layer requires you to serialize all the layer’s properties, such as the layer name, the layer visibility flag, folder names, and so on.
 
The in-memory data of the dynamic layer can be serialized to the project file (.3DD) or to a user-defined cache (XML document, enterprise database, ArcGIS feature class, and so on). It is also reasonable to assume since the data is dynamic, it is not necessary for it to be serialized.
 
The following code shows implementing IPersistStream Save and Load to serialize a layer:
 

[C#]
void ESRI.ArcGIS.esriSystem.IPersistVariant.Save(IVariantStream Stream)
{
 Stream.Write(m_name);
 Stream.Write(m_bValid);
 Stream.Write(m_bCached);
 Stream.Write(m_dblMinScale);
 Stream.Write(m_dblMaxScale);
 Stream.Write(m_bDrawDirty);
 Stream.Write(m_bShowIcon);
                
  Stream.Write(m_spRef);
 Stream.Write(m_extent);

 Stream.Write(m_extensions);
} 

void ESRI.ArcGIS.esriSystem.IPersistVariant.Load(IVariantStream Stream)
{
 m_name              = (string)Stream.Read();
 m_bValid             = (bool)Stream.Read();
 m_bCached          = (bool)Stream.Read();
 m_dblMinScale      = (double)Stream.Read();
 m_dblMaxScale     = (double)Stream.Read();
 m_bDrawDirty       = (bool)Stream.Read();
 m_bShowIcon       = (bool)Stream.Read();

 m_spRef              = (ISpatialReference)Stream.Read();
 m_extent             = (IEnvelope)Stream.Read();

 m_extensions        = (ArrayList)Stream.WiringRead();

 //Allocate a new vector 3D, do not worry about writing and reading...
 m_vector3D          = new Vector3DClass();
}
 
The following code shows serializing (caching) the in-memory data into an XML document:
 

[C#]
/// <summary>
/// The main update thread of the layer.
/// </summary>
private void ThreadProc()
{
  . . .
  
   //Serialize the tables onto the local machine.
  DataSet ds = new DataSet();
  //Add the in-memory tables to the data set.
  ds.Tables.Add(m_table);
  ds.Tables.Add(clonedTextureMap);
  //Write the data set to the local machine.
  ds.WriteXml(m_weatherXmlFile);
  
   ds.Tables.Remove(m_table);
  ds.Tables.Remove(clonedTextureMap);
  clonedTextureMap.Dispose();
  ds.Dispose();
  GC.Collect();
}

 

Labeling objects

 
In a 2D mapping application, labeling objects is one of the most important functionalities. Labeling an operation to achieve high-quality cartographic output can become fairly complex. Sophisticated algorithms are implemented inside ArcGIS Labeling Engine or Maplex. In 3D GIS, full-feature labeling can become extremely complex. This document discusses labeling techniques commonly used in 3D. The focus is the real-time rendering of text strings in 3D space.
 
Creating fonts for fast 3D display
 
Typically, fonts are 2D representations of characters in vector or raster formats. Font types dictate the drawing performance, especially when rendered in 3D. Using OpenGL, character representation can be done using outline fonts, bitmap fonts, or texture fonts. Outline fonts are rendered using vector techniques, while bitmap fonts and texture fonts are raster techniques. Outline fonts are typically used when text needs to be rotated freely around x,y and z-axes. Bitmap fonts do not allow text rotation and scaling by depth. While still maintaining the performance, texture fonts do not have these limitations presented by the bitmap fonts.
 
Creating billboard text
 
One of the most common ways to draw legible 3D labels is to make each label facing the viewer or the camera viewing plane. This means the label will be drawn parallel to the screen regardless of the viewing direction and position.
 
Implementing a HitTest method
 
A HitTest method is used on a control (TOCControl, MapControl, GlobeControl, etc.). When you click a control, it gives you the object or item where the control was clicked. When implementing ICustomGlobeLayer, the HitTest method is addressed through the ICustomGlobeLayer::Hit method. Implementation of this method is essential for selection, identifying, editing, and other custom functionality that requires you to interactively select or locate your object in the globe display.
 
When drawing an object, always use glLoadName() to replace the value on the top of the OpenGL name stack. The name stack is used during an OpenGL selection mode to allow sets of rendering commands to be uniquely identified. It consists of an ordered set of unsigned integers.
 
As shown in the following code, the value passed to glLoadName() should always be a unique unsigned integer identifier of the drawn element, such as the object ID or any number that can uniquely identify it:
 

[C#]
. . .
//Load the name to implement the HitTest method.
GL.glLoadName((uint)lZipCode); 
//Draw the item.
GL.glCallList(m_billboardRectList);
 
The ArcGlobe framework uses this mechanism to pass the HitTest method the unique ID of the hit object as an input argument. You have to use this identifier to get the identified object from the layer’s data structure and populate the object’s information in the Hit3D object that is also passed to the method as an argument. The information on the hit object is passed back to the caller through a PropertySet, which is assigned back to the Hit3D object.
 
As shown in the following code, ensure the hit object belongs to the current layer:
 

[C#]
//Get the owner.
object owner = pHit3D.Owner;
       
//Ensure the owner is a weather layer.
if (!(owner is RSSWeatherLayer3DClass))
 return;
 
Use the unique ID passed by the method to locate the identified object in the layer’s data structure. See the following code example:
 

[C#]
//Get the record by the ZIP Code received from the selection buffer. 
DataRow[] rows = m_table.Select("ZIPCODE = " + hitObjectID.ToString());
if (rows.Length == 0)
  return;
 
Ensure the Hit3D point is set. Otherwise, set it according to the object’s geometry. See the following code example:
 

[C#]
//Get the hit point from the Hit3D.
IPoint hitPoint = pHit3D.Point;

//Ensure the hit point is initialized.
if (null == hitPoint)
{
   double lat, lon, alt;
   lat = Convert.ToDouble(r[3]);
   lon = Convert.ToDouble(r[4]);
   alt = 1000.0;

   IPoint point = new PointClass();
   ((IZAware)point).ZAware = true;
   point.PutCoords(lon, lat);
   point.Z = alt;

   pHit3D.Point = point;
}
 
Populate a PropertySet with the object’s information and assign it back to the Hit3D object. This PropertySet is received by the caller object (the one that triggered the ICustomGlobeLayer::Hit method). The caller object should use the information in the PropertySet to get the relevant information on the object.
 
For that reason, you must pass all the information that is relevant on the object as shown in the following code:
 

[C#]
//Create a new PropertySet instance.
IPropertySet propSet = new PropertySetClass();

//Add the object’s information to the PropertySet.
propSet.SetProperty("ZIPCODE",         r[1]);
propSet.SetProperty("CITYNAME",       r[2]);
propSet.SetProperty("LATITUDE",        r[3]);
propSet.SetProperty("LONGITUDE",      r[4]);
propSet.SetProperty("TEMPERATURE",  r[8]);
propSet.SetProperty("DESCRIPTION",   r[9]);
propSet.SetProperty("DAY",               r[11]);
propSet.SetProperty("DATE",             r[12]);
propSet.SetProperty("LOW",              r[13]);
propSet.SetProperty("HIGH",              r[14]);
propSet.SetProperty("UPDATED",        r[16]);
 
Finally, pass the PropertySet to the Hit3D object:
 

[C#]
//Pass the PropertySet to the caller.
pHit3D.Object = propSet;

 

Identifying objects

 
The standard globe Identify tool displays additional information that cannot be shown in the globe display to the user of the Identify dialog box. This makes the implementation of your layer more complete. For your layer to support ArcGlobe and the GlobeControl standard Identify tool, it must implement the Identify interface, which declares the Identify method called by the framework when the layer was identified. The parameters passed to the method are the geometry of the identified point and an array that is populated with the identify results. See the following example:
 

[C#]
public IArray Identify(IGeometry pGeom)
{
. . .

}
 
Given the geometry of the identified Hit place, you should query the center of its bounding envelope and convert the point into the window’s coordinate. See the following code example:
 

[C#]
IEnvelope inEnv;
IArray array = new ArrayClass();

//Get the envelope from the geometry.
if (pGeom.GeometryType == esriGeometryType.esriGeometryEnvelope)
  inEnv = pGeom.Envelope;
else
  inEnv = pGeom as IEnvelope;

if (inEnv.IsEmpty)
  return array;

//Get the envelope’s center coordinate.
double xMin, xMax, yMin, yMax, zMin, zMax;
inEnv.QueryCoords(out xMin, out yMin, out xMax, out yMax);
zMin = inEnv.ZMin;
zMax = inEnv.ZMax;

double xC, yC, zC;
xC = (xMin + xMax) * 0.5;
yC = (yMin + yMax) * 0.5;
zC = (zMin + zMax) * 0.5;

ISceneViewer sceneViewer = m_globeDisplay.ActiveViewer;
ICamera camera = sceneViewer,Camera;

//Convert the coordinate into the window’s coordinate.
ISceneViewer sceneViewer = m_globeDisplay.ActiveViewer;
IGlobeViewUtil globeViewUtil = (IGlobeViewUtil)sceneViewer.Camera;

int winX, winY;
globeViewUtil.GeographicToWindow(xC, yC, zC * 1000.0, out winX, out winY);
 
Once you convert the identified point into the window’s coordinate, call IGlobeDisplay::LocateMultiple to get all objects at the given coordinate (these are all of the objects in the globe display that support the HitTest). See the following code example:
 

[C#]
IHit3DSet hits;
//Locate all items that fall within the given location.
m_globeDisplay.LocateMultiple(sceneViewer, winX, winY, true, false, false, false, out hits);

if (null == hits)
   return array;
 
Next, iterate through the set of returned Hit3D objects and verify that each object returns an IPropertySet (which is returned by ICustomGlobeLayer::Hit).
 
For each of these objects, create an instance of IdentifyObject, pass it the PropertySet, and add it to the output array of identified results. See the following code example:
 

[C#]
IHit3D           hit3D      = null;
IPropertySet     propSet    = null;
IIdentifyObj     idObj      = null;
IIdentifyObject  idObject   = null;
bool             bIdentify  = false;

//Get all the hits from the Hit3Dset.
IArray objArray   = hits.Hits;
int    nCount     = objArray.Count;

//Iterate through the hit items and create identify objects.
for (int i = 0; i < nCount; i++)
{
   hit3D = objArray.get_Element(i) as IHit3D;  
   
//Make sure that the hit object type is a PropertySet.
   propSet = hit3D.Object as IPropertySet;
   if (null != propSet)
   {
//Instantiate the identify object and add it to the array.
      idObj = new GlobeWeatherIdentifyObject();
      
//Test whether the layer can be identified.
      bIdentify = idObj.CanIdentify((ILayer)this);
      if (bIdentify)
      {
         idObject = idObj as IIdentifyObject;
         idObject.PropertySet = propSet;
         array.Add(idObj);
      }
   }
}

//Return the array with the identify objects.
return array;
 
This document does not cover how to create Identify objects. For a sample that implements the IdentifyObject method, please refer to RSS Weather 3D Layer.
 
Selection of objects
 
The ability to select an object by an attribute or location is one of the most common tasks a GIS layer must support. Selection results should be visual, with a highlighted symbol to indicate the item is selected, as well as an indication inside the layer’s data structure that designates the item is selected.
 
To highlight selected items, you can use any symbol to draw instead of or in addition to the object’s standard symbol. Inside the DrawImmediate drawing method while drawing the layer’s object, test whether an object is selected and add the selection symbol if it is.
 
See the following code example:
 

[C#]
GL.glPushMatrix();
   //Translate to the item’s location.
   GL.glTranslatef(Convert.ToSingle(X), Convert.ToSingle(Y), Convert.ToSingle(Z));
   
   //Orient the icon so that it will face the camera.
   OrientBillboard();

   //Scale the item (original size is 1 unit).
   double useScale = 0.04 * dMagnitude;
   GL.glScaled(useScale, useScale, 1.0);

   GL.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
   //Loads the zip code onto the name stack.

   GL.glLoadName((uint)lZipCode);
   //Draw the item.
   GL.glCallList(m_billboardRectList);

   //If the object is selected, draw a box around it.
   if (bIsSelected)
   {
      GL.glColor4ub(255, 255, 128, 255);
      GL.glCallList(m_selectionDisplayList);
   }

   . . .
GL.glPopMatrix();
 
Selection by attribute is straightforward. You have to iterate through your data structure and set the selection flag. You may want to add a new selection to an existing selection set, select from a selection set, revert selection, and so on. However, you will need to implement the code that does this type of selection against the layer’s data structure.
 
The following code shows selecting objects according to an object’s ZIP Code (unique):
 

[C#]
public void Select(long zipCode, bool newSelection)
{
   //If it is a new selection, unselect any selected items.
   if (newSelection)
   {
   //Unselect all the currently selected items.
      lock (m_table)
      {
        foreach (DataRow r in m_table.Rows)
        {
          r["SELECTED"] = false;
        }
          m_table.AcceptChanges();
      }
   }
      
   //Get the record from the table.
   DataRow[] rows = m_table.Select("ZIPCODE = " + zipCode.ToString());
   
   //Make sure the record exists.
   if (rows.Length == 0)
     return;

   DataRow rec = rows[0];
   //Set the selection flag to true.
   lock (m_table)
   {
     rec["SELECTED"] = true;
     rec.AcceptChanges();
   }
}
 
Selection by area mechanism is based on the implementation of ICustomGlobeLayer::Hit. This allows the user to call IGlobeDisplay::Locate or IGlobeDisplay::LocateMultiple, which in turn uses ICustomGlobeLayer::Hit to get a list of all objects that “hit” the selection geometry.
 
The following C# code snippet demonstrates selection by area:
 

[C#]
public override void OnMouseDown(int Button, int Shift, int X, int Y)
{
if(null == m_weatherLayer)
   return;

object owner;
object obj;

IPoint hitPoint;
//Get the globe display.
IGlobeDisplay globedisplay = ((IGlobe)m_scene).GlobeDisplay;

ISceneViewer sceneViewer = globedisplay.ActiveViewer;

//Call locate to HitTest the element in the current window coordinate.
globedisplay.Locate(sceneViewer, X, Y, false, false, out hitPoint, out owner, out obj);

if(obj != null)
{
   //Make sure the returned object is indeed a PropertySet.
   if(!(obj is IPropertySet))
   {
     return;
   }
   //Cast the object into a PropertySet.
   IPropertySet propset = obj as IPropertySet;
   if(propset == null)
     return;
                     
    //Make sure the owner is the right layer.
   if(!(owner is RSSWeatherLayer3DClass))
     return;   
                        
      //Get the ZIP Code.
   object o = propset.GetProperty("ZIPCODE");
   long zipcode = Convert.ToInt64(o);

     //Select the object.
   m_weatherLayer.Select(zipcode, Shift != 1);
}
}
 
Flashing objects in the globe display
 
When you are identifying objects in ArcGlobe or GlobeControl, the objects usually “flash” in the globe display (alternating their color or changing their shape for a few seconds). It is also possible that you will have to flash objects for other reasons in your application.
 
There are several ways to set the flash symbol for an object: using another selection symbol that gets drawn instead of or in addition to your object’s symbol, using OpenGL selection buffer, and so on. However, each of these methods requires you to alternate your symbol.
 
The best way to alternate the symbol is by using counters. Considering that a layer would has a global timer to redraw the globe display at a constant pace, you can set a normal object’s symbol on even drawing cycles and set a flash symbol on odd drawing cycles.
 
The following code demonstrates using static counters to flash an item using a dedicated display list:
 

[C#]
private long          m_lItemToFlash  = -1L;
private static int   m_flashCount    = 0;
private static bool m_flashDraw     = true;

//Flash the required object.
if (lZipCode == m_lItemToFlash)
{
  if (m_flashCount > 10)//quit flashing after 10 draw cycles
  {
      m_lItemToFlash = -1;
      m_flashDraw = true;
      m_flashCount = 0;
  }
  else //Draw the flash symbol.
  {
     //Draw only on even cycles.
     if (m_flashDraw == true)      
     {
        GL.glCallList(m_intFlashSymbolList);
     }
    
      //Set the counter. 
      m_flashCount++;

     //Alternate the flash drawing.
     m_flashDraw = !m_flashDraw;
  }
}
 
Implementing prediction mode
 
In most cases, your custom layer would only have to reflect changes given by the real-time feed it listens to. For example, if a vehicle position gets updated every two minutes, then the vehicle is stationary in the globe display in between these updates.
 
In some cases, you might want to show the items moving in the globe display to give a more realistic scenario (it is not logical that airplanes stand still in the sky) or to give the user better information, even if it means displaying an estimated location.
 
The basic idea of prediction mode is to get the object’s position and motion parameters, such as heading, velocity, acceleration, roll, and pitch, and on each draw cycle unless there is an update from the real-time feed to “drive” the object into its estimated location. It is legitimate to make assumptions that the object is constantly moving and does not accelerate, that the heading is constant or the object does not change its elevation. All of this is according to the requirements dictated by the application, as well as the availability of the information.
 
To place an object correctly, you will have to know its speed and heading. This information is usually given by the server (even if you have to calculate the heading according to two update cycles of the object). To calculate the new object’s position you need to know the time elapsed between the previous update and the current update. This time can be driven from the layer’s redraw timer. However, it may not be as accurate, since when you interactively navigate in the globe display or use the animation framework, the globe’s refresh changes and does not comply with the redraw timer. For that reason, the best solution is to store together with the object’s info and the precise time of the last drawing for the object and calculate the elapsed time as the time span between the previous and the current time.
 
The next code example shows calculation of the distance to the next object’s position according to the object’s speed and the time elapsed:
 

[C#]
//Get the system time.
long ticks = DateTime.Now.Ticks;

//Get the cached last update time.
long ticksSinceUpdate = r["UPDATETIME"];

//Get the object’s speed.
double speed = ref["SPEED"];

//Calculate the distance to the next object position; time is in seconds. 
dDist = speed * ((double)(ticks - ticksSinceUpdate) / 1000.0);

r["UPDATETIME"] = ticks;
. . .
 
You should pay attention to the distance units used to move the object to its predicted location. In cases where you are using geographical decimal degrees to set your object’s position, you must get the distance in decimal degrees as well. Usually, speed is given in miles per hour, kilometers per hour, knots, and so on. To convert these linear units into decimal degrees, you can rely on the given globe radius using the Angle [rad] = Distance/Radius connection.
 
The following code sample demonstrates conversion from kilometers per hour into decimal degrees:
 

[C#]
//Get the globe radius.
IGlobeDisplayRendering globeDisplayRendering = (IGlobeDisplayRendering)m_globeDisplay;
double globeRadiusMeters = globeDisplayRendering.GlobeRadius;

//Calculate the angular distance in radians. 
double dblDistDD = (dblDistMeters/(globeRadius + dblAltMeters));

//Convert the distance from radians to decimal degrees.
dblDistDD *= (180.0/ Math.PI);

 

Additional resources for .NET developers

 
ArcGIS 9.2 .NET SDK includes additional resources integrated into the Visual Studio IDE designed to simplify and speed up development. These resources include BaseClasses and ready-to-use templates.
 
CustomGlobeLayer abstract class
 
As part of ArcGIS 9.2, a new abstract (BaseClass) that implements GlobeCustomLayer has been added to the ESRI.ArcGIS.ADF.BaseClasses assembly. The new BaseClass is named GlobeCustomLayerBase. The class is available for .NET developers and can be used in a similar way as BaseCommand and BaseTool.
 
GlobeCustomLayerBase implements and supplies default functionality for all required interfaces to implement custom layers for globe, including serialization through the Save and Load methods. To implement your class by inheriting GlobeCustomLayerBase, you must override the DrawImmediate() method, which is declared as an abstract method.
 
GlobeCustomLayerBase also inherits from the System.Windows.Forms.Control class and allows you to call the Invoke method to avoid cross apartment calls, while implementing timer event handlers or other multithreading. The class also has a built in ADO.Net DataTable as its underlying data structure with built-in functionality such as:
 
 
See the following illustration:
 
 
Custom layer IDE integration template
 
ArcGIS 9.2 also provides .NET developers an ArcGIS IDE Integration framework that provides wizard-based implementation of commands, tools, kiosk applications, category registration, and much more.
 
One of the command items provided by ArcGIS IDE Integration is a template that implements a CustomGlobeLayer based on the abstract BaseClass added to ESRI.ArcGIS.ADF.BaseClasses. The template creates a new class that inherits from GlobeCustomLayerBase and overrides the abstract DrawImediate method, as well as other methods required to use the layer.
 
For more information about ArcObjects in ArcGIS 9.2, developers can visit the ESRI Developer Network (EDN) Web site at http://edn.esri.com/.
 
For general information about ESRI products and services, visit http://www.esri.com.
 


See Also:

How to get and install an OpenGL wrapper for .NET
How to draw a geographical object on the globe using direct OpenGL plug-in
Using direct OpenGL in order to digitize on the globe surface
RSS Weather Layer 3D