A primer on foregoing Cesium’s native labels in favor of using custom HTML labels to enhance user experience, and our developer experience getting to the current implementation.


Background

As the amount of user-defined data grew in our application, the ability to discern contextual relevance became increasingly important. In a map-centric application especially, making available that discernible data quickly and intuitively can prove challenging. That is, geospatial visualizations—by nature—need ancillary and tangential experiences to provide context both beyond the globe and in minutiae.

In choosing Cesium (for many reasons, the greatest of which being 3D and terrain support), our initial needs were met for displaying small amounts of data per map object by, namely, Cesium’s standard label element. Through several style iterations and continuous experience improvements, however, the standard labels began to feel out of place.

Our color palette and typography had been honed for affordance and hierarchy, as well as to accommodate the environment in which the application would be used (e.g., light levels and screen size). Simply put, we needed more control over label-like elements in order to provide a similar, justifiable experience.

Standard Cesium
Labels

Standard Cesium labels

Custom HTML Labels

The current form of our custom HTML labels

Exploration

Thinking inside the bounds of Cesium, we explored using billboards as replacements. The difficulty faced with billboards is that they expect rasterized data, such as images, and our desire was to use markup to be truly flexible. By using SVG’s foreignObject tag, we were able to render markup as an SVG, which in turn could be encoded as the source for an image.

We worked through several iterations of this method, at times utilizing raw SVG strings, canvas, window.btoa, and base64 encoding before landing on the output of encondeURIComponent as the image source.

The example below shows creating an image for use; a later example will show how it relates to Cesium.

<svg xmlns="http://www.w3.org/2000/svg" height="55px" width="300px">
    <foreignObject height="55px" width="300px">
        <div xmlns="http://www.w3.org/1999/xhtml">
            [CONTENT]
        </div>
    </foreignObject>
</svg>
function createImage(svgAsString, onLoadFn) {
    var image = new Image();
    image.onload = onLoadFn;
    image.src = 'data:image/svg+xml,' + encodeURIComponent(svgAsString);
    return image;
}

Outcome & Refinement

For more static conditions, this method worked well. Performance concerns were always present for more dynamic labels, though, because for any data change, the image would need to be completely redrawn and rendered. For the most part, browser inconsistencies were manageable. For instance, we temporarily used promises to handle setting image.src to account for instantiation time variance between Chrome and Firefox. The breaking point came when IE11 was deemed the primary browser for client use. URI-encoded SVGs are supported by IE11, but the crux of this solution—the foreignObject tag—is not.

In exploration unrelated to labels, we found that appending HTML and positioning via Cesium utilities worked surprisingly well. Aside from having to handle positioning (which could be abstracted) the benefits were incredible: updatable elements (no destroying), entirely customizable, sharper rendering, and unrestricted browser support.

function createLabel(coordinate, htmlAsString) {
    var context = this;
    // Create an empty `div` that will serve as the label container
    context.labelShape = document.createElement('div');
    // Append the label container to the map container
    // See note at the end of the post for an explanation of `cesiumAdapter`
    cesiumAdapter.getViewer().container.appendChild(context.labelShape);

    context.labelShape.style.position = 'absolute';

    // Set the internal content of the label
    context.labelShape.innerHTML = htmlAsString;

    // Set the position of the label by transforming the Cesium coordinates to
    // `x` and `y` window positions
    updateDomPosition(coordinate, context.labelShape);
}

function updateDomPosition(coordinate, labelShape){
    // This function also gets recalled each time the object's coordinates change
    // See note at the end of the post for an explanation of `cesiumShapeHelper`

    // Get coordinates as Cartesian3
    var cartesianPoint = cesiumShapeHelper.coordinateAsCartesian(coordinate);
    // Get `window` position from Cartesian3 coordinates
    var windowPosition = Cesium.SceneTransforms.wgs84ToWindowCoordinates(cesiumAdapter.getViewer().scene, cartesianPoint, {});
    // Set label position via CSS
    if (windowPosition) {
        labelShape.style.top = windowPosition.y + 'px';
        labelShape.style.left = windowPosition.x + 'px';
    }
}
<div class="c-cesiumContentWrapper">
    <div class="c-labelContent">
        <div class="c-labelText">[CONTENT]</div>
    </div>
    <div class="c-labelCaret"></div>
</div>

Considerations

What is not shown in the above example is handling changes in label positioning. As mentioned, in using this approach we need to handle updating label positions outside of Cesium’s normal methods. To do this, we utilize a registry of point coordinates that gets compared to the previous registered set on a timer. Cesium’s Clock onTick event is what we currently use to on-loop discern position changes, which in turn runs the updateDomPosition function from the example. We’ll discuss implications of this method later in the post.

As with all HTML-in-JS cases, the more complex the labels become, the more we needed logical templates. Handlebars offered all we needed, simply passing the output of Handlebars’ compilation to the element’s innerHTML.

<div class="c-cesiumContentWrapper">
    <div class="c-labelContent">
        <div class="c-labelText">}</div>
        {{#if secondaryText}}
            <div class="c-labelText c-labelText--secondary u-pt">{{{secondaryText}}}</div>
        {{/if}} 
    </div>

    <div class="c-labelCaret"></div>
</div>

Outro & Notes

The experiential value afforded to users with the introduction of HTML labels has been positively noted. We are able to provide several different contextual labels depending on interaction type (i.e., hover vs. click; shift-click vs. left-click) that would not have truly been possible otherwise.


Implications:

  • The manner in which we chose to handle position updates is still under consideration and evaluation. Using a timer works well in all cases we’ve encountered, including when hundreds of points are rendered on the map. Our reservations lie with using onTick, which would cease to run if the clock was paused.
  • We currently use absolute positioning via CSS for label placement, but it has been noted that CSS translate transforms could provide better performance for large change batches.

Notes:

  • In this particular application, we actually allow for using multiple mapping libraries (Leaflet being the other, currently). cesiumAdapter and cesiumShapeHelper are abstraction APIs that help us normalize similar methods in each library. This is very much beyond the scope of this post, so suffice to know that cesiumAdapter.getViewer simply returns the current Cesium Viewer instance, and cesiumShapeHelper.coordinateAsCartesian simply returns the provided coordinates as Cartesian3 coordinates.
  • All CSS class names adhere to the SUIT CSS naming convention. We use an internal CSS framework and a UI guide system that we hope to write about in a future post.