ArcGIS SDK  

Extending ArcObjects

Point Dispersal Renderer Example

In this section:

  1. The case for a point dispersal renderer
  2. Creating the PointDispersalRenderer
  3. Renderer property pages

Point dispersal renderer example

Object Model Diagram Click here.

Example Code Click here.

Description This project enables a point feature layer to be drawn with the features moved so that none of their symbols overlap. An accompanying property page allows the properties of the renderer to be set via the ArcMap user interface.

Design PointDispersalRenderer is a subtype of the FeatureRenderer abstract class. PointDispersalPropertyPage implements the standard property page interfaces.

License required ArcView or above.

Libraries Carto, CartoUI, Display, DisplayUI, Framework, Geodatabase, Geometry, and System

Languages Visual Basic

Categories ESRI Renderer Property Pages

Interfaces IFeatureRenderer, IPersistVariant, ILegendInfo, IComPropertyPage, IComEmbeddedPropertyPage, and IRendererPropertyPage.

How to use

    1. Register PointDispersalVB.dll and double-click the PointDispersalVB.reg file to register to component categories.
    2. Open ArcMap and add a few layers to the map—make sure at least one layer contains point features. You can use the 'dispersalrenderer_miscpoints' shapefile in the Samples/Data/ExtendingArcObjects folder of the ArcGIS Developer Kit.
    3. Zoom the map until you have difficulty seeing the individual points as they overlap.
    4. Right-click the layer that has point features in the table of contents and click Properties. In the Layer Properties dialog box, click the Symbology tab.
    5. Click the custom renderers category and click Point Dispersal Renderer.
    6. Select the properties you want for your renderer, and click OK to dismiss the dialog box.

      Your layer will now draw using the point dispersal renderer.


The case for a point dispersal renderer

Imagine that you have a point feature class where many of the points lie close to or on top of each other. You would like to force the points to all display individually, even if it means that the point is drawn slightly away from its true location.

Some point datasets have features close to or an top of each other. A custom feature renderer can be developed to disperse the point symbols so all the features can be seen.

Before describing the custom feature renderer solution, it is worth noting that there are many different approaches to this problem. A custom layer is probably a more complete solution, since for this particular problem the custom renderer will result in the selection tools not working properly.

Another approach to dispersing the points would be to label the features with a single character, placed directly over the feature; the labelling functionality could then be used to avoid overlapping labels, although in this scenario it is hard to ensure all features are labelled.

Creating a subtype of FeatureRenderer

By reviewing the Display object diagram, you can see that all renderer classes inherit from the FeatureRenderer abstract class. Therefore, any type of custom renderer you create should begin by implementing the IFeatureRenderer interface, along with interfaces for cloning and persistence.

You can see that there are a few other interfaces that are commonly implemented by a renderer, such as IRendererFields, IRotationRenderer, IBarrierProperties2, IDataExclusion, IDataNormalization, ILookupSymbol, and ITransparencyRenderer. You will not need to implement any of these interfaces in this example; however, you can find more information on implementing these interfaces at the end of this example.

In the DisplayUI object diagram, you can also see that each renderer has an associated property page class. In the ArcMap user interface, this not only allows a user to assign an instance of the custom renderer to a layer, but also to alter the properties of the renderer.

Creating the PointDispersalRenderer

To answer the display requirements described above, you will create a custom feature renderer called PointDispersalRenderer, that disperses the points as necessary to avoid their symbols overlapping.

You will also provide an accompanying property page implementation for your class.


Implementing IFeatureRenderer

The IFeatureRenderer interface is the core of a renderer. The main method that will be called by the ArcGIS framework is Draw, at which point, it is the job of your renderer to draw the feature layer in any way you specify.

The Draw method receives a reference to the Display to which the renderer should draw and also a feature cursor indicating the features to be drawn. Start by identifying the Symbol you will be drawing the feature with; this is stored in the LegendGroup for this renderer (see the Implementing ILegendInfo section for more information).

[Visual Basic 6]
Private Sub IFeatureRenderer_Draw(ByVal Cursor As IFeatureCursor, _
 ByVal drawPhase As esriDrawPhase, ByVal Display As IDisplay,
 ByVal trackCancel As ITrackCancel)
  ...
  Dim pSym As ISymbol
  Set pSym = m_pLegendGroup.Class(0).Symbol
  Display.SetSymbol pSym
  ...
  Dim pFeature As IFeature
  Set pFeature = Cursor.NextFeature

Draw also receives a trackCancel parameter, which indicates if the user has pressed the Esc key to cancel the drawing. This is important, since the point dispersal could become slow in extreme situations with large datasets. This is a suitable point to check the cancel tracker. You should also check this cancel tracker at the end of the main drawing loop.

[Visual Basic 6]
Dim bContinue As Boolean
If Not trackCancel Is Nothing Then bContinue = trackCancel.Continue

The Draw method provides the main functionality of a renderer; Draw receives a reference to a FeatureCursor, which contains all the features the renderer should draw.
You should check the cancel tracker and stop drawing if it indicates the user has pressed Esc.

Implement the main Draw method loop by iterating through the feature cursor, taking each feature in turn, and drawing to the specified Display by calling the PlaceFeature function to find the dispersed location chosen for the feature.

[Visual Basic 6]
Do While (Not pFeature Is Nothing) And (bContinue = True)
  Dim pPoint As IPoint
  Set pPoint = pFeature.Shape
  ...
  PlaceFeature pPoint, 0, pGeomColl, Display, pSym, pPlacedPoint, _
  pSymPoly, dDispersalDist
  ...
  pGeomColl.AddGeometry pSymPoly.Envelope
  Display.DrawPoint pPlacedPoint
  ...
  Set pFeature = Cursor.NextFeature
  If Not trackCancel Is Nothing Then bContinue = trackCancel.Continue
Loop

Each time a feature is drawn, add the Envelope of the feature to a GeometryBag variable, which holds the extent of all the dispersed points placed so far and is passed in to the PlaceFeature function each time to allow the function to identify where features have previously been placed.

Drawing the features

Note that the actual drawing is done with IDisplay::SetSymbol and then, for each feature, IDisplay::DrawPoint. This is not typical for a custom renderer; it is general practice for the renderer to pass the relevant symbol to the IFeatureDraw::Draw method on the feature. Calling IFeatureDraw::Draw allows custom features to use their own drawing methods. In the case of the point dispersal example, the rendering is incompatible with IFeatureDraw since the feature is to be drawn potentially away from its true location.

Generally, custom renderers should draw the features on the display by calling IFeatureDraw::Draw.

If drawing directly to the display, there is no need to call IDisplay::StartDrawing and IDisplay::FinishDrawing, since you are already inside a drawing phase started by the ArcGIS framework.

PlaceFeature function

The actual dispersal of the points is done in the PlaceFeature function. A geometry bag of symbol envelopes is cached, which records all the currently drawn points on the display.

Each feature is first placed at its true location. If the symbolized feature overlaps any of the already drawn features in the layer, then a new attempt to place it is made at a certain distance away.

All four points of the compass are tried, and then the dispersal distance is increased until the feature is eventually placed, and its envelope added to the geometry bag.

PlaceFeatures calculates a new location for a feature, so it does not overlap other features that have already been drawn.

Layer draw phases

There are three draw phases for a layer—geography, annotation, and selection. Except for the selection phase, the Draw method of a renderer will be called for each phase that you specify in IFeatureRenderer::RenderPhase.


[Visual Basic 6]
Private Property Get IFeatureRenderer_RenderPhase(ByVal drawPhase As _
 esriDrawPhase) As Boolean
  If drawPhase = esriDPGeography Then
    IFeatureRenderer_RenderPhase = True
  Else
    IFeatureRenderer_RenderPhase = False
  End IfEnd Property

Draw is then also called for the selection phase if there are selected features on the display. The example chooses to ignore this phase, since the features are dispersed from their true locations and thus incompatible with the selection tool. Incidentally, raising E_FAIL from your Draw routine for a selection phase will result in the default selection rendering for the layer.

For an example of a renderer that uses the annotation phase, see the 'BivariateRenderers' sample in the ArcGIS Developer Help.

Drawing during debugging

There is one more thing to mention about drawing features to the display. If you are debugging your custom renderer with Visual Basic 6, the features will not display on the map, as the Display has a process-dependent device context. See Chapter 2, 'Developing Objects', for more information on debugging.

With the Visual Basic 6 debugger, the features drawn by a custom renderer will not appear on the display.

Preparing the query filter

Before IFeatureRenderer::Draw is called, you are given an opportunity to modify the query filter that produces the feature cursor in the PrepareFilter method. In this method, you must add into the filter any fields you need for your renderer. The point dispersal renderer does not rely on any particular attributes.

The PrepareFilter method gives you a chance to specify which fields your renderer needs to perform drawing.

However, the ExclusionSet property allows the framework to specify that a renderer should exclude a certain set of features from drawing.

[Visual Basic 6]
Private Property Set IFeatureRenderer_ExclusionSet(ByVal pFeatureIDSet _
 As IFeatureIDSet)
  Set m_pExclusionSet = pFeatureIDSet
End Property

Therefore, if there is a set of features that have been specified in ExclusionSet, you need to make sure that the object ID field name is added to the QueryFilter parameter.

[Visual Basic 6]
Private Sub IFeatureRenderer_PrepareFilter(ByVal fc As IFeatureClass, _
 ByVal QueryFilter As IQueryFilter)
  If Not m_pExclusionSet Is Nothing Then
    If m_pExclusionSet.Count > 0 Then
      QueryFilter.AddField fc.OIDFieldName
    End If
  End If
End Sub

If you implement IFeatureRenderer::ExclusionSet, you must ensure the object ID field is fetched with PrepareFilter.

Although the PointDispersalRenderer example implements IFeatureRenderer::ExclusionSet, it is unlikely that you would find that an exclusion set is used with this renderer in ArcMap, since the ExclusionSet is mainly related to the Convert Features to Graphics command, which is inappropriate with this renderer.

Other client programs however may make their own exclusion sets. Note that if clients call IFeatureRenderer::ExclusionSet directly it will be ignored, since the feature layer exclusion set overrides the renderer exclusion set. If you use IGeoFeatureLayer::ExclusionSet this will be passed down to the renderer.

The query filter is used by the ArcGIS framework to produce the feature cursor passed to the Draw method. It is actually a spatial filter (you could QI for ISpatialFilter to prove this to yourself), with the display extent being used to limit which features are returned.

You will find that there are normally more features in the cursor than are within the display extent, since the spatial filter criteria is set against the spatial index rather than the feature geometries. It is more efficient for the renderer to draw these offscreen features than have a slower query. In the case of data that does not have a spatial index (for example, some shapefiles), you will find all the features in the dataset are present in the feature cursor.

You may find features in the cursor that are not within the current display extent. It is generally more efficient to draw these features than to check their extent yourself. ArcGIS has produced the query for speed of execution.

For layers with feature class extensions or custom features, the query filter may already have some subfields set, as it is the feature layer rather than the renderer that is responsible for checking IFeatureClassDraw::RequiredFieldsForDraw.

CanRender property

If you want to restrict which layers your custom renderer can be applied to, such as being applicable only to line layers, then in your implementation of IFeatureRenderer::CanRender, you can test properties of the feature layer and return True if your renderer supports it and False if it does not.

The CanRender property should indicate if a renderer can draw a certain FeatureClass.

In your code, ensure the PointDispersalRenderer can only be applied to point layers.

[Visual Basic 6]
Private Function IFeatureRenderer_CanRender(ByVal featClass _
 As IFeatureClass, ByVal Display As IDisplay) As Boolean
  If featClass.ShapeType = esriGeometryPoint Then
    IFeatureRenderer_CanRender = True
  Else
    IFeatureRenderer_CanRender = False
  End If
End Function

Other types of renderers may check for other things about the FeatureClass or Display references passed in—for example, a renderer specially designed for networks may check if the FeatureClass contains a particular type of network feature by checking the IFeatureClass::FeatureType property.

SymbolByFeature method

The SymbolByFeature method should return the symbol appropriate to a given feature. For the PointDispersalRenderer, this is simple, since the point dispersal renderer only uses one symbol for all features—note that you can only return the original locations of the features.

SymbolByFeature is called repeatedly by the ArcMap Convert Features to Graphics tool, and hence this command, when called on a layer symbolized with a PointDispersalRenderer, will generate graphics in the original feature locations.

SymbolByFeature should return the symbol the renderer would use to draw a specific, individual feature.

Using SymbolByFeature also enables the possibility of containing other renderers within your custom renderer. Imagine that in the example, you would like to disperse the points, but instead of a single symbol, you use one of other symbology options such as proportional symbols or unique values. This could be achieved by keeping a reference to a contained renderer class, your custom renderer, then for each feature in the Draw loop, calling SymbolByFeature on the contained renderer to determine the symbol to use. In the ArcGIS Developer Help, you can see that the 'BivariateRenderers' sample custom renderer operates in this way.

Implementing ILegendInfo

ILegendInfo is often quite straightforward to implement. This interface ensures the table of contents and legends are able to show a list of the symbols, labels, and headings your renderer is using. In the IFeatureRenderer::Draw method, you have already seen how you can reuse the existing LegendGroup and LegendClass objects and use these to hold the symbols with which your custom renderer will draw.

ILegendInfo helps link a renderer with the table of contents.

Declare a member variable to hold a reference to a LegendGroup.

[Visual Basic 6]
Private m_pLegendGroup As ILegendGroup

Use m_LegendGroup to return the values of the LegendGroupCount, LegendGroup, and LegendItem properties. The LegendGroup is set up in the class initialization code and by the IDispersalRenderer interface—see the example code project for full details.

Return False from SymbolsAreGraduated, and do not allow this property to be changed, as you will not implement any symbol graduation functionality to the PointDispersalRenderer.

Creating and implementing IDispersalRenderer

You need to provide a way for clients to change the Symbol used by the PointDispersalRenderer and also the dispersal distance.

Create an interface called IDispersalRenderer, with two read-write properties, DispersalRatio and Symbol. Implement IDispersalRenderer in the PointDispersalRenderer coclass. For more information on how you can create a new interface, see Chapter 2, 'Developing Objects'.

[Visual Basic 6]
Private Property Set IDispersalRenderer_Symbol(ByVal pSymbol As ISymbol)
  Set m_pLegendGroup.Class(0).Symbol = pSymbol
End Property

The custom IDispersalRenderer interface provides access to the symbol and dispersal distance used by the renderer.

The Symbol property is set into the first Class of the LegendGroup, which means that the LegendGroup contains the correct symbol and will display correctly in a legend or table of contents. Note that the Symbol property is passed by reference.

[Visual Basic 6]
Private Property Let IDispersalRenderer_DispersalRatio(ByVal RHS As Double)
  m_dDispersalRatio = RHS
End Property

The dispersal ratio value is used by the PlaceFeature function, as described previously. You will create a renderer property page, which will be the main consumer of IDispersalRenderer.

Implementing persistence

You must implement the standard persistence interface or interfaces, to preserve the state of the renderer in a map document (.mxd) or layer file (.lyr). Implement IPersistStream and IPersistStream if using VC++ or IPersistVariant if using VB.

A renderer must be persistable.

In your PointDispersalRenderer, you need to save the legend group, which is the dispersal distance ratio. Any objects you persist must implement IPersistStream (as does the legend group) or IPersistVariant.

[Visual Basic 6]
Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)
  'Persistence version number
  Stream.Write m_lCurrPersistVers
  Stream.Write m_pLegendGroup
  Stream.Write m_dDispersalRatio
End Sub

See Chapter 2, 'Developing Objects', for more information about the version checking used in the PointDispersalRenderer persistence code.


Renderer property pages

Implementing a custom renderer property page will allow users to interact with the settings of your custom renderer. By registering the property page in the ESRI Renderer Property Pages component category, the page will appear on the Symbology tab of the Layer Properties dialog box along with all the standard symbology options. The Symbology tab is itself a property page; therefore, your property page needs to be an embedded property page.

Define your custom renderer property page as a class called PointDispersalPropertyPage, that implements the standard interfaces for an embedded property page and the IRendererPropertyPage interface.


Design your UI on a form as shown below—you can place all the controls and descriptive text for the main part of the page onto another control, which has a window handle (the example project uses a Picture box control). Reference this form through a private data member in the PointDispersalPropertyPage class.

Implementing property page interfaces for the PointDispersalPropertyPage

The interfaces implemented on a property page class are dependent upon your development environment; refer to Chapter 2, 'Developing Objects', for general information on implementing property page interfaces. This discussion will follow the use of the interfaces implemented in the VB example project.

The Applies method may not actually be called for an embedded page; however, it is best practice to implement this method fully anyway. Return True if you find a PointDispersalRenderer.

[Visual Basic 6]
Objects.Reset
Set pObj = Objects.Next
Do While Not TypeOf pObj Is IDispersalRenderer
  Set pObj = Objects.Next
  If pObj Is Nothing Then
    IComPropertyPage_Applies = False
    Exit Function
  End If
Loop

In the SetObjects method, you are passed a set of objects—you should find the renderer in this list, check it is a PointDispersalRenderer, then initialize the controls on the accompanying form using the properties of the supplied renderer.

[Visual Basic 6]
Dim pObj As Variant
  Set pObj = Objects.Next
  Do Until pObj Is Nothing
    If TypeOf pObj Is IDispersalRenderer Then
      Set m_pRenderer = pObj
      m_frmPage.InitControls m_pRenderer
    End If
  Set pObj = Objects.Next
  Loop

The Applies and SetObjects methods should both check that they are passed a reference to a PointDispersalRenderer.

The PointDispersalRenderer property page only requires a reference to the renderer itself. However, the object set passed to SetObjects will also include the map, feature layer, and feature class. If you are adapting this example to create a different kind of renderer, you may need these references to allow users to set the properties of the renderer correctly.

The Apply method is triggered when the user clicks Apply or OK on the layer properties property sheet. After calling this method the framework will set the renderer supplied in SetObjects as the live renderer. As shown in the example project code, you can use the implementation of IComEmbeddedPropertyPage::QueryObject to apply the changes to the renderer object.

[Visual Basic 6]
Private Sub IComPropertyPage_Apply()
  IComEmbeddedPropertyPage_QueryObject m_pRenderer
End Sub

Use IComPropertyPage::Priority to control where your renderer appears in the listbox of available renderers. Use a lower number to have your renderer and category appear toward the top of the list (the priority of the first page in a category controls where that category fits in the list). Generally, you should use a high number for custom renderers to ensure they display after the standard renderers—for the PointDispersalPropertyPage, return a value of 600.

The table below lists standard renderer property pages and their priorities.

TypeNamePriority
FeaturesSingle symbol100
CategoriesUnique values200
Unique values, many fields210
Match to symbols in a style300
Graduated symbols310
Proportional symbols320
Dot density330
ChartsPies400
Bars410
Stacked420
AttributesQuantity by category500

Implementing IComEmbeddedPropertyPage

Custom renderer property pages fall into the class of embedded property pages. In VC++ you should ensure you implement the CreateCompatibleObject and QueryObject members of IPropertyPageContext; in VB you will need to implement IComEmbeddedPropertyPage.

In ArcMap, users choose from different symbology options from the tree view on the Layer Properties Symbology tab. Because the internal representation of each option is a different renderer object, as the user chooses a new option, a new renderer is being edited. In some cases, properties are preserved during this transition. For example, when a user switches between the Bar chart and Pie chart options, the renderer fields and symbols are preserved from the old to the new renderer.

As you will create an embedded property page, the properties of your renderer can be preserved when users switch between types of renderer.

In addition to managing the retention of properties from an old renderer, you should also use CreateCompatibleObject to avoid excessive cloning of renderers. In this method check to see if the in parameter is an object of the type your page should edit. If so, return that same object. If not, create and return a new renderer object of the proper type.

[Visual Basic 6]
Private Function IComEmbeddedPropertyPage_CreateCompatibleObject(ByVal kind As Variant) As Variant
  
  Dim pDispersalRend As IDispersalRenderer
  If TypeOf kind Is IDispersalRenderer Then
    Set pDispersalRend = kind
  Else
    Set pDispersalRend = New PointDispersalVB.Renderer
    ...
  End If
  Set IComEmbeddedPropertyPage_CreateCompatibleObject = pDispersalRend
End Sub

If you want, you can also copy any compatible properties you can find.

[Visual Basic 6]
    ...
    Set pDispersalRend = New PointDispersalVB.Renderer
    If TypeOf kind Is ILegendInfo Then
      Dim pLegendInfo As ILegendInfo
      Set pLegendInfo = kind
      If pLegendInfo.LegendGroupCount > 0 Then
        If TypeOf pLegendInfo.LegendGroup.Class(0).Symbol Is IMarkerSymbol Then
          Set pDispersalRend.Symbol = pLegendInfo.LegendGroup.Class(0).Symbol
        End If
      End If
    End If
    ...

CreateCompatibleObject should return a PointDispersalRenderer. You can attempt to copy any compatible properties from the renderer reference passed in to the PointDispersalRenderer.

In the QueryObject method, apply the changes made on the property page to the supplied object. This renderer will become the live renderer for the layer.

[Visual Basic 6]
Private Sub IComEmbeddedPropertyPage_QueryObject(ByVal theObject As Variant)
  Dim pRenderer As IDispersalRenderer
  If Not theObject Is Nothing Then
    If (TypeOf theObject Is IDispersalRenderer) Then
      Set pRenderer = theObject
      m_frmPage.ApplyToRenderer pRenderer
    End If
  End If
End Sub

Typically, a property page creates a temporary object and allows changes to this object. Then, if the Apply or OK buttons are clicked, the temporary renderer replaces the 'live' renderer object on the feature layer. If the Cancel button is clicked, then the temporary renderer is discarded.

The ArcGIS framework automatically creates the temporary renderer by cloning the renderer on the layer before passing it to your page, so it is not necessary for your code to make a copy. Note that this cloning will make use of the persistence code for your renderer, as renderers do not support IClone.

Implementing IRendererPropertyPage

All renderer property pages implement an additional interface IRendererPropertyPage. Some of its properties will appear on the ArcMap Symbology property page, which will be the container of your property page when displayed in the Layer Properties dialog box. These properties help guide users when accessing your custom page.

The Description string will appear at the top of the parent page, and the PreviewImage will appear in the bottom left of the dialog box. A preview image size of 116 by 88 pixels will display at a 1:1 ratio—if the image is larger or smaller, it will be scaled to fit the preview box. IRendererPropertyPage::Name appears in the tree view on the left side of the symbology property page.

For IRendererPropertyPage::Type, use 'Custom Renderers', so that your renderer displays in the same category as the other renderer developer samples. If you use an already existing Type (for example, 'Features'), your renderer will appear under that category, listed by Priority order.

IRendererPropertyPage is used to edit the items on a property page that are common to all renderers. The Preview property gives users an idea of how a renderer will display.

The Type property dictates where your renderer will appear in the listbox of available renderers. The Name property is used to display an entry for the custom renderer in the list.

In IRendererPropertyPage::CanEdit you should check the in parameter to make sure your custom page can edit the specified renderer. Typically, your custom property page will only edit your custom renderer. For the PointDispersalPropertyPage, you can check for the presence of the IDispersalRenderer interface to identify your renderer.

[Visual Basic 6]
Private Function IRendererPropertyPage_CanEdit(ByVal obj As _
 IFeatureRenderer) As Boolean
  If TypeOf obj Is IDispersalRenderer Then
    IRendererPropertyPage_CanEdit = True
  Else
    IRendererPropertyPage_CanEdit = False
  End If
End Function

If you create a different kind of renderer, it is well worthwhile defining an interface, which will uniquely identify your renderer to help you implement CanEdit.

Note that standard renderers also implement CanEdit in this way, which has implications for the interfaces you might like to implement on a custom renderer.

For example, it might make sense for the point dispersal renderer to implement ISimpleRenderer, since all of its methods and properties are appropriate. However, this would cause problems with the property page. The Features/Single Symbol property page will return True from CanEdit for any renderer that implements ISimpleRenderer. As this page has a higher priority than the custom property page, the wrong page would be shown for a point dispersal renderer. In practice, it is straightforward to avoid implementing the interfaces that identify the standard renderers as they are all named similarly to their coclasses.

Each renderer has an interface that identifies it to its property page.

Now you are ready to use your renderer. See the Managing custom feature renderers topic for further advice.

 


Back to top

Go to example code

See Also Customizing the Display, About custom feature renderers, and Managing custom feature renderers.