In this topic
- About implementing globe layers
- Drawing simple 3D graphics
- Manipulating feature layers
- Drawing 3D objects using GlobeGraphicsLayer
- Using GlobeGraphics API for custom drawings
- Advanced drawing
- Implementing custom globe layers
- Preparing applications to draw 3D graphics on a globe
- Broadcasting activities through IGlobeDisplayEvents
- Communicating with the GlobeDisplay
- Implementing a 3D custom layer
- Setup for drawing OpenGL
- Loading 3D models from file
- Creating display lists from 3D symbols
- Coordinate systems for drawing 3D graphics
- Globe coordinate system
- Local object’s coordinate system
- Coordinate transformation
- Aligning the object's up direction
- Drawing sequence
- Optimizing the drawing sequence
- Minimizing the computation load inside DrawImmediate
- Scaling and aggregating symbols
- Screening out objects outside the display viewport
- Using a single timer to redraw the display
- Managing dynamic objects
- Connecting to real-time feeds
- Serializing the custom layer
- Labeling objects
- Identifying objects
About implementing globe layers
ArcGlobe enables continuous three-dimensional (3D) visualization of spatial data ranging from global to local scale. ArcGlobe can efficiently display terabytes of high-resolution images and 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, text, images, and 3D models. This topic reviews the different ways to display this 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 and has been supported on most operating systems and hardware platforms.
All code examples in this topic, can be found in the sample, RSS weather 3D layer.
Drawing simple 3D graphics
ArcGlobe provides 3D rendering functionality by feature layer and also allows the drawing of 3D graphics by user interface (UI) 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. It is possible to apply transformation to features belonging to a feature class.
Drawing 3D objects using GlobeGraphicsLayer
In ArcGlobe, 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 and 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 similar to the existing API of ArcMap and the MapControl for managing graphic elements. You can use different types of graphic elements with 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 possible 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 an overhead because each graphic element needs to have its own symbol and geometry. These graphic elements need to be flexible in terms of usability and 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 example shows how to create a graphic layer and add it to the globe:
//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 example shows how to create a graphic element and add it to the graphic layer:
//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 following code example shows how to update a graphic element inside a graphic layer:
//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. The GlobeCore API facilitates advanced 3D static and dynamic drawing.
Implementation methods
The following are the main methods to add user-defined drawings to the globe:
- Drawing within the content of a command item
- 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 do not require management. 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
Developers can use a direct OpenGL plug-in to draw a user-defined object on the globe's display. Developers can 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 the 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;
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 objects on the globe, and so on.
Implementing custom globe layers
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, computer-aided design (CAD) files, image files, and feature classes, which are stored in a geodatabase.
You might have a custom or unsupported data format that you want to display on 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.
The following are the main types of custom layers that can be implemented by a class that implements ICustomGlobeLayer:
- Rasterized (esriGlobeCustomDrawRasterize)—Layer type rasterized by the layer's Draw method and usually serves custom raster layers.
- Rasterized by tile (esriGlobeCustomDrawByTile)—Layer for the custom data that is rasterized and rendered directly into the tile. Globe calls the GetTile() method to obtain the rasterized data for displaying on the globe's display.
- Custom OpenGL layer (esriGlobeCustomDrawOpenGL)—Layer type draws 3D objects using the direct OpenGL plug-in. This is the layer type discussed in this topic and is suitable for custom graphics drawing.
Issues for implementing a globe custom layer
There are many ways to implement a custom layer. Before implementing your layer, review the following issues that can affect the architecture of your layer:
- Type of input feed and database for the layer.
- Amount of objects managed by the layer.
- Geographic distribution of the data. Are the objects distributed homogeneously?
- Is the extent of the layer large or limited to a local area?
- What is the rate at which objects get added, updated, and deleted from the layer?
- What type of geometry is used by the layer?
- What functionality does your layer have to support (dynamic selection, editing capabilities, and so on)?
Preparing applications to draw 3D graphics on a globe
Globe's drawing pipeline is different and more sophisticated than typical two-dimensional (2D) graphic drawings on ArcMap or a MapControl 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 (use the AfterDraw() method).
Broadcasting activities through IGlobeDisplayEvents
The GlobeDisplay broadcasts some of its activities through IGlobeDisplayEvents. The AfterDraw() event is broadcasted after the drawing activity is done. The application can listen to these events by implementing IGlobeDisplayEvents on a class (for example, a custom layer, a tool's coclass).
- The following code example shows how the AfterDraw event is wired:
//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 code example shows how to draw mouse feedback in IglobeDisplayEvents.AfterDraw:
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();
Communicating with the GlobeDisplay
When you are implementing a command or a tool that requires you to listen to GlobeDisplayEvents, you can use the GlobeHookHelper class. 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.
Implementing a 3D custom layer
The GlobeCore API provides the ICustomGlobeLayer interface for more advanced 3D graphic drawings. Any layer 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 and the GlobeControl. Developers do not have to listen to GlobeDisplayEvents to do drawings. Instead, ICustomGlobeLayer provides a method (DrawImmediate) where all drawings should take place.
- The following code example shows wiring AfterDraw inside the ICommand.Onclick event handler using GlobeHookHelper:
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 to pay attention to when implementing a custom layer are DrawType() and DrawImmediate(). Implement the Hit() method to support identify, picking, and selection functions. Details and code examples for each method are discussed later in this topic.
Usually, 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 following remaining required interfaces to implement a layer for ArcGIS:
- ILayer
- IGeoDataset
- ILayerExtensions
- ILegendInfo
- ILayerDrawingProperties
- ILayerInfo
- IPersistStream
The .NET software development kit (SDK) includes an abstract BaseClass that implements a CustomGlobeLayer in addition to a CustomGlobeLayer template that overrides the relevant methods of the BaseClass allowing rapid development.
Setup for drawing OpenGL
To draw inside the globe display, set the symbology for your drawing and 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, use an Open Source managed wrapper library for OpenGL or directly import the relevant methods into your assembly.
Loading 3D models 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 example shows loading a 3D model from file:
//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:
//Create a 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 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, scale the symbol so that it appears 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 on the globe display.
- See the following code example that shows how to calculate the symbol scale:
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.
Coordinate systems for drawing 3D graphics
To draw 3D graphics using the OpenGL API, 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 (for example, 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 code example that shows how to calculate the scale for a globe coordinate system:
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):
IGlobeViewUtil, a utility interface, allows you to convert coordinates for each of the coordinate systems used by globe.
- The following code example shows how to convert coordinates for each coordinate system using globe:
//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, 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, 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 results in 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 example aligns the object's up direction to the globe's normal vector. The code example 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 illustration shows how the adjustment makes the objects display right-side up. Without proper orientation, objects can appear upside down in some locations.
Drawing sequence
Drawing takes place when the GlobeDislay redraws. 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 ICustomGlobeLayer.DrawImmediate method. 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, and the drawing should take place in the DrawImmediate call. The basic drawing sequence should have the following steps:
- 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.
- 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 World Geodetic System 1984 (WGS84) geographical coordinate system.
- Once you have your object’s coordinate in a WGS84 geographical coordinate system, convert it into a geocentric coordinate system by using the IGlobeViewUtil interface.
- 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.
- Translate the object into place and if necessary, orient the object.
- Scale the object to display in its real size or if necessary, aggregate the object’s size.
- Call the display list of the relevant symbol in which to draw.
- Call glPopMatrix to pop back the current matrix stack.
- The following code example shows a simple implementation of the DrawImmediate call:
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 and the GlobeControl is usually done for real-time applications. Usually, such applications are required to match a 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 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
- Scaling and aggregating the drawn symbology
- Screening out objects outside of the display viewport
- Using a global timer to redraw the display
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, cache its calculated geocentric coordinate and orientation so it can be used inside the DrawImmediate method.
- The following code example shows how to minimize the computation load:
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 code example shows calculating the scale (magnitude) according to the distance from the camera to the object using geocentric units, taking into consideration the elevation exaggeration:
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 example shows how to set the scale threshold:
//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, 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 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 example shows testing an object inside the viewport:
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 example filters out objects outside of the viewport:
//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 you do not 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, and the amount of objects that you have to address in the application.
Since the 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 an object gets updated and the time 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 code example shows a class inheriting the System.Windows.Forms.Control interface:
//Class definition.
public abstract class GlobeCustomLayerBase: Control, ILayer, IGeoDataset, . . .
//Class constructor.
public GlobeCustomLayerBase()
{
//Call CreateControl to create the handle.
this.CreateControl();
}
. . .
- The following code example shows using the timer to redraw the GlobeDisplay:
//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 can consume all 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 might 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 screen shot of a C++ active template library (ATL) class declaration code shows how the global timer singleton is declared:
- The following code example using the global timer class is straightforward:
//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 lot of importance to the underlying data structure used to manage dynamic data. Dynamic data might stream in from an external feed, synchronically or asynchronously. As a developer, you might 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 and 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, a HashTable, or choose to implement your layer using a generics collection.
- The following code example shows a user-defined data structure that is storing real-time data using ADO.NET DataTable:
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 example shows accessing the data stored in the DataTable:
//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 example shows acquiring exclusive lock to support thread safety:
//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 might have to use in your custom layer. You might 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 the following two methods:
- Push mode—Means the layer listens to events sent by the server resulting in a synchronized connection.
- Pull mode—Means the layer queries the server periodically for updates, normally by using a timer.
Optimizing performance, especially when connecting to a Web service after getting a response from the server, might take a long time. Moving the connection to a background thread might 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 example shows pulling messages from an ArcGIS Tracking Server feed:
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 (Extensible Markup Language [XML] document, enterprise database, ArcGIS feature class, and so on). It is also reasonable to assume since the data is dynamic, that it is not necessary for it to be serialized.
- The following code example shows implementing IPersistStream Save and Load to serialize a layer:
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 example shows serializing (caching) the in-memory data into an XML document:
/// <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 dataset.
ds.Tables.Add(m_table);
ds.Tables.Add(clonedTextureMap);
//Write the dataset 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 topic 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 draws 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, and so on). 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 example, the value passed to glLoadName() should always be a unique unsigned integer identifier of the drawn element, such as the ObjectID 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 example, ensure the hit object belongs to the current layer:
//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:
//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:
//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 example:
//Create a 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. See the following code example:
//Pass the PropertySet to the caller.
pHit3D.Object = propSet;
Identifying objects
The standard globe Identify tool displays additional information that cannot be shown on 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 code example:
[C#]
public IArray Identify(IGeometry pGeom)
{
. . .
}
- Given the geometry of the identified Hit place, query the center of its bounding envelope and convert the point into the window’s coordinate. See the following code example:
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 objects in the globe display that support the HitTest). See the following code example:
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;
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:
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 topic does not cover how to create Identify objects. For a sample that implements the IdentifyObject method, see the previously referenced RSS weather 3D layer sample.
Selecting 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, and 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:
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 faces 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 might want to add a new selection to an existing selection set, select from a selection set, revert selection, and so on; however, implement the code that does this type of selection against the layer’s data structure.
- The following code example shows selecting objects according to an object’s ZIP Code (unique):
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 code example shows selection by area:
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 the color or changing the 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 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 example shows using static counters to flash an item using a dedicated display list:
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 only has to reflect changes given by the real-time feed it listens to. For example, if a vehicle position gets updated every two minutes, 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 and the availability of the information.
To place an object correctly, you need 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 elapsed time between the previous update and the current update. This time can be driven from the layer’s redraw timer. However, it might 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 following code example shows calculation of the distance to the next object’s position according to the object’s speed and the elapsed time:
//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;
. . .
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. 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 example shows conversion from kilometers per hour into decimal degrees:
//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);
See Also:
How to get and install an OpenGL wrapper for .NETHow to draw a geographical object on the globe using direct OpenGL plug-in
How to draw mouse feedback on the globe using direct OpenGL plug-in
Sample: RSS weather 3D layer
Development licensing | Deployment licensing |
---|---|
Engine Developer Kit | Engine Runtime: 3D |
ArcView: 3D Analyst | ArcView: 3D Analyst |
ArcEditor: 3D Analyst | |
ArcInfo: 3D Analyst |