Once the style objects have been defined, you need to specify to which objects they apply. In the simplest case, you use the same style, independent of the object. In any other case, you will want to base style selection on intrinsic properties of the object. The base interface that allows you to do all of this is ILspStyler.

Objects are styled through a callback mechanism: a painter object, for instance, needs information about the styles associated with a set of objects it needs to paint, and calls ILspStyler.style(…​). The set of objects are passed to the styler as arguments, along with a ALspStyleCollector. The ILspStyler implementation determines which styles go with the supplied object set, or a subset of that object set. The styler then uses the methods of the ALspStyleCollector to apply the styles to the appropriate objects.

The styling API allows you to specify to which geometry styling needs to be applied. This can be a geometry that is entirely different from the original object’s geometry. The ALspStyleCollector.geometry(…​) method is the most direct way to specify a different geometry in the form of an ILcdShape object, and does not require you to write a custom class. This can be quite convenient in a number of cases. It may not be the best choice in terms of optimization, however, mainly because it forces the implementation to keep track of the shape, for each object.

A better way is to use ALspStyleTargetProvider implementations that can derive the shape from the object, and pass such an implementation to the ALspStyleCollector.geometry(…​) method. This ensures that the shape is only created when it is needed, and avoids the bookkeeping overhead of associating one or more shapes with each object.

Creating a basic ALspStyler

Whenever your data does not allow you to apply the same style to all objects in a layer, you probably need to write a custom ALspStyler implementation. To get you started, it is recommended that you have a look at the available samples and documentation.

Implementing a basic ALspStyler is quite straightforward, but there are a couple of points that require more attention. Here is an example that shows what such an implementation can look like:

Program: A simple styler
public class MyStyler extends ALspStyler {

  private ALspStyle style1;
  private ALspStyle defaultStyle;

  @Override
  public void style( Collection<?> aObjects, ALspStyleCollector aStyleCollector, TLspContext aContext ) {
    for ( Object object : aObjects ) {
      if ( hasStyle1( object ) ) {
        aStyleCollector.object( object ).style( style1 ).submit();
      } else {
        aStyleCollector.object( object ).style( defaultStyle ).submit();
      }
    }
  }

  private boolean hasStyle1(Object aObject){
    ...
  }
}

Quite often, this type of implementation works just fine, but consider the following questions:

Is my implementation thread-safe?

The styler can be used from any thread, so the ILspStyler implementation should implement the necessary safeguards against any potential threading issue.

Is my implementation efficient enough?

Depending on the nature of the data, the number of times your styler is called may vary. For instance, if you have a model containing 100000 elements that are updated every 100 milliseconds, you may want to think about avoiding an iteration over all objects, to maximize throughput. For more tips to improve the performance of your stylers, see Improving the performance of your stylers.

Choosing ILspStyler implementations

Beside the abstract base class, LuciadLightspeed also offers a number of concrete implementations that cover common use cases. These help you get started, and provide optimal implementations wherever possible. This section discusses most of the available implementations.

Your choice of ILspStyler type depends on the following criteria:

  • Do I want to use styling to differentiate between objects, or do all objects require the same style?

  • Does the object styling remain the same, or can the application users change the styling properties?

  • Does the geometry I want to style my object with match the original geometry of the object? For more information, see Deriving geometry from objects.

Even though almost any styler can be written as a hierarchy of these basic implementations, such an approach may not be desirable. To increase performance, you should consider limiting the total number of calls to ALspStyleCollector. This is hard to achieve if you are delegating the styling of one object to a large number of separate stylers. The right implementation depends a lot on the type and the amount of your data, so the general rule of implementing functionality first, and optimizing for performance if necessary also applies here.

Using one style for all objects: ALspStyle

If only one style needs to be applied to all objects, simply use the style itself. It implements ILspStyler directly. Its implementation of ILspStyler just returns one style for all objects.

Defining multiple styles for all objects: TLspStyler

TLspStyler is a convenience implementation that allows you to specify a set of styles. You can define multiple styles for each object, a line and a fill style for polygons for instance. A TLspStyler object passes all of these styles directly, regardless of the object it is called with. This is very efficient, because it does not require any iterations over objects.

It also supports the use of ALspStyleTargetProvider, which allows basic filtering on objects. As a result, you can already achieve quite complex styles using only this class, a few basic styles, and an ALspStyleTargetProvider.

Using mutable stylers: ILspCustomizableStyler

The basic ILspStyler interface provides no control over the styles that are being used. The styles are not exposed, so you cannot simply change the styles. This contrasts with the requirements of many applications to allow users to change styling properties. To meet these requirements, LuciadLightspeed offers the ILspCustomizableStyler interface, which can be implemented by objects that allow style changes. It ensures that these components change styles in the same same way.

Implementations of ILspCustomizableStyler expose their styles as a modifiable list of TLspCustomizableStyle objects. You can modify the style objects in the list to adapt, enable or disable certain styles. To remove or add styles, you can modify the list itself.

A TLspCustomizableStyle is a wrapper around a plain ALspStyle. It adds some additional properties that allow for easy identification and integration in a user interface. Contrary to the regular ALspStyle object it is wrapping, a TLspCustomizableStyle object is a mutable object. Whenever the wrapped style object is replaced by a different style object, the TLspCustomizableStyle object fires the necessary events. When a style collector requests the styles it needs to apply, the wrapped immutable ALspStyle object is sent to the collector.

TLspCustomizableStyler

A default implementation of ILspCustomizableStyler is provided by TLspCustomizableStyler. Its behavior is very similar to that of TLspStyler, but it adds the advantages provided by ILspCustomizableStyler. The most important additional feature is the ability to activate and deactivate styles. You can use this to toggle a fill or line style for a given layer, for example. For more information about TLspStyler, see Defining multiple styles for all objects: TLspStyler.

Changing styles per object: TLspEditableStyler

If you want the style of individual objects on the map to be defined by the end user of your application, you can use TLspEditableStyler. It contains a default set of styles for all objects, and also keeps a mapping from objects to more specific styles.

Do not use this class if your style can be derived from the properties of the object. It is mostly suited if the end user can randomly assign styles to individual objects.

Note that this class does not support the changing of a style for a given category of objects. For that, you should have a look at ILspCustomizableStyler which is discussed in Using mutable stylers: ILspCustomizableStyler.

Toggling stylers

LuciadLightspeed provides concrete and abstract ILspStyler classes that support the delegation of objects to one of two stylers.

ALspToggleStyler

splits objects in two sets and applies separate stylers to each set.

TLspDrapingToggleStyler

toggles stylers to distinguish between drapable objects and non-drapable objects.

Dynamically applying styles

LuciadLightspeed fully supports, and is optimized for styles that change arbitrarily. This opens up a wide range of possibilities, such as animations, scale ranges and highlighting. To get an idea of what can be achieved with these dynamic effects, run the corresponding samples.

The most important thing to remember when implementing a styler that changes its styles, is to fire a style-changed event whenever your style changes. This is the only way to ensure that your style change is taken into account. For optimal performance, the event should also include the objects that are affected by the style change, to prevent another query to the style to retrieve the affected objects.

The only exception to this rule are style changes due to object changes: if a model-changed event is fired, the styles of the affected objects are queried again automatically. The styler does not need to fire events in this case.

Animated styles

The performance offered by LuciadLightspeed allows you to implement animations by gradually changing the properties of a style. You can for instance change the opacity of the styles for a given object from 0.0 to 1.0 to achieve a fade-in effect.

Have a look at the lightspeed.customization.style.animated sample for a possible implementation.

To achieve a smooth animation, your styler implementation needs to be called quite often. Hence, the performance and thread safety of your implementation is very important in the case of animations. A typical styler that is suited for animation is shown in Program: More efficient styler implementation..

Deriving geometry from objects

Sometimes you do not want to apply styles to an object as it occurs in your ILcdModel. Situations where you may want to apply a different styling are:

  • The object consists of separate parts, and you want to apply a distinct styling to each part.

  • The object does not directly implement an ILcdShape, but contains one. This case is known as a "has-a-shape" situation. You want to style the object so that it uses that shape.

  • You want to paint a geometry that does not correspond to the geometry of the domain object. You could create and display pie charts for country objects, for instance.

countrypiechart
Figure 1. Displaying pie charts for country objects with the lightspeed.customization.style.highlighting sample.

For these types of cases, you can implement ALspStyleTargetProvider. Use the side-effect method getStyleTargetsSFCT() to derive geometries for a given object. These geometries are usually ILcdShape instances that can be used together with the associated styles. Program Program: A simple ALspStyleTargetProvider for "has-a-shape" domain objects. shows a simple implementation that replaces a specific type of object with its contained shape.

Program: A simple ALspStyleTargetProvider for "has-a-shape" domain objects.
ALspStyleTargetProvider customTargetProvider = new ALspStyleTargetProvider() {
    @Override
    public void getStyleTargetsSFCT( Object o, TLspContext aContext, List<Object> aObjects ) {
      if ( o instanceof CustomShape ) {
        aObjects.add( ( ( CustomShape ) o ).fShape );
      }
      else {
        aObjects.add( o );
      }
    }
  };
TLspStyler styler = new TLspStyler();
styler.addStyles( customTargetProvider, Collections.singletonList( lineStyle ) );

This class can also be used as a very basic filter: if you do not return any geometry for the given object, the object will not be visible. You cannot implement complex rules with this class, however.

Implementers of ALspStyleTargetProvider must make sure that the equals() and hashCode() methods are correctly implemented. If the geometry that is to be used changes, a different ALspStyleTargetProvider instance that is not equal to the previous one should be provided. Of course, if the object itself changes, and a proper model-changed event is thrown, the ALspStyleTargetProvider will be queried again to obtain an updated geometry.

Ensuring correct selection and culling results

By default, the shape layer only retrieves those model objects that are visible within the view, and leaves out the others. This culling activity occurs during object painting and during user interactions such as selection. It is key to maintaining efficiency when you are working with large or slow models like databases. During the culling process, the model bounds of objects implementing ILcdBounded are used to determine whether an object should be considered or not.

However, it is possible that you are using a styler to produce geometry that does not necessarily correspond to these bounds. You paint a circle with a radius of 1km around a point object, for example. The point object has size 0, while the circle covers a region with a 2km size. By default, the circle will not be painted if the point lies outside of the view. Users cannot select it either.

To prevent this type of situation, set a culling margin in pixels or meters when you are building your shape layer. You can use the objectViewMargin or objectWorldMargin for this purpose. The shape layer extends the considered view region with these margins when it is retrieving domain objects. In the example of the circle, a world margin of 1km results in the desired behavior: the circle is still painted.

The view margin is particularly useful when you are painting icons for point objects. Whereas the point object has a size of 0, the icon can have a size of 64 by 64 pixels for example. A view margin of 32 pixels ensures that an icon is painted even if the center point falls just outside of the view.

Using an ALspStyleTargetProvider in your ILspStyler

The ALspStyleCollector allows you to specify the ALspStyleTargetProvider that is to be used in combination with a number of styles.

Program: How to specify a ALspStyleTargetProvider
aStyleCollector.object(myObject)
 .geometry(myFillStyleTargetProvider)
 .style(myFillStyle)
 .submit();
aStyleCollector.object(myObject)
 .geometry(myLineStyleTargetProvider)
 .style(myLineStyle)
 .submit();

As you can see in Program: How to specify a ALspStyleTargetProvider, it is possible to specify multiple style target providers for the same object. Note that myFillStyleTargetProvider and myLineStyleTargetProvider may be compared to each other, and considered the same if they are equal.

Example: Styling the points in a list with different icons

This example shows how you can implement a style target provider to apply different icon styles to points in a list, depending on the position of the points in the point list. As you can see, this ALspStyleTargetProvider implementation has an fIndex field that indicates which point needs to be retrieved from the incoming object. If such a point can be found, it is added to the result.

Program: Specifying a specific geometry. (from samples/lightspeed/common/tracks/TrackHistoryPointProvider)
@Override
public void getStyleTargetsSFCT(Object aObject, TLspContext aContext, List<Object> aResultSFCT) {
  final EnrouteAirwayTrack track = (EnrouteAirwayTrack) aObject;
  if (track.isAirborne()) {
    final ILcdPoint point = track.getTrackHistory().safeGetPoint(fIndex);
    if (point != null) {
      aResultSFCT.add(point);
    }
  }
}

Example: Using styling to convert a point to a circle

Suppose that you want to draw a circle with a radius of 10km around your current position on the map. You can easily achieve this with a simple line style and a style target provider.

Program: Converting geometry
public void getStyleTargetsSFCT( Object aObject, TLspContext aContext, List<Object> aResultSFCT ) {
  ILcdEllipsoid ellipsoid =  ((ILcdGeoReference)aContext.getModelReference()).getGeodeticDatum().getEllipsoid();
  aResultSFCT.add( new TLcdLonLatCircle( (ILcdPoint) aObject, 10000, ellipsoid) );
}

Improving the performance of your stylers

This section provides recommendations for the improvement of your stylers' performance.

Maximizing object throughput

In the example in Creating a basic ALspStyler, an iteration over all objects was used to specify the styling. While this is a perfectly usable implementation that may be sufficient in a lot of cases, there is still room for improvement.

Suppose that all of your objects share the same styles, but one specific object requires a special style. The following sample shows how you can apply that style efficiently. In the sample, a collection of objects possibly contains animated objects, which require a styling that is different from the styling of the regular, unanimated objects.

Program: More efficient styler implementation. (from samples/lightspeed/customization/style/animated/AnimatedAreaStyler)
@Override
public void style(Collection<?> aObjects, ALspStyleCollector aStyleCollector, TLspContext aContext) {
  //use contains to determine if the animated object is part of the collection in the most efficient way
  if (aObjects.contains(fAnimatedObject)) {
    if (aObjects.size() == 1) {
      aStyleCollector.object(fAnimatedObject).styles(fAnimatedStyle).submit();
    } else {
      final Collection<Object> objectsWithoutAnimated;
      if (aObjects instanceof ILcdCloneable) {
        //some LuciadLightspeed collection implementations such as TLcdIdentityHashSet
        //can be cloned to allow efficient copying
        objectsWithoutAnimated = (Collection<Object>) ((ILcdCloneable) aObjects).clone();
      } else {
        objectsWithoutAnimated = new TLcdIdentityHashSet<Object>((Collection<Object>) aObjects);
      }
      // remove the animated object
      objectsWithoutAnimated.remove(fAnimatedObject);
      aStyleCollector.object(fAnimatedObject).styles(fAnimatedStyle).submit();
      aStyleCollector.objects(objectsWithoutAnimated).styles(fDefaultStyle).submit();
    }
  } else {
    aStyleCollector.objects(aObjects).styles(fDefaultStyle).submit();
  }
}

This program takes only a few more lines to write, but is much more efficient: we only submit special styles if the list of objects to be styled contains the fAnimatedObject.

In this sample, it is assumed that the collection of objects that is passed into the styler supports efficient lookups. This is a safe assumption if the styler is used in the default LuciadLightspeed layers and painters. Note as well the use of ILcdCloneable, which allows for efficient cloning. This is also something that is safe to assume in LuciadLightspeed, and can result in performance improvements.

Minimizing the number of submit() calls

Another common styling performance issue can occur if you use multiple submit() calls per object to specify styles, although you can specify the same styling with less calls. The basic idea behind styling objects in batch is that only the person who defines the styling, knows best how to match objects to styles. If the styler implementation does not take advantage of this knowledge, the painter will get to the same result eventually, but might need more time to achieve the result.

A first opportunity for optimization presents itself when your objects can be divided in categories: you have four types of objects and want to associate a style with each one of them, for instance. You can loop over all objects, and submit the appropriate styles per object. This works just fine, but it may be a better idea to split the objects into four groups first, and then submit the styles per group. Again, the performance impact of this mostly depends on how many times the styler needs to be used.

A second example of optimization consists of specifying two separate geometries for a given object. The geometries need to be styled with the same style. A basic implementation could use ALspStyleCollector.geometry(myShape) in two separate submit() calls with the same style. In a better implementation, you could either write an ALspStyleTargetProvider implementation that returns both geometries at once, or you could use a TLcdShapeList that contains both geometries as an argument for the geometry(…​) call, so that you only need to call submit() once.