Bing Maps Geodesics

This month’s MSDN magazine has an article describing how to create curved lines on the Bing Maps AJAX control. While I don’t want to criticise the author at all, there are two comments I would make on the article:

  • Firstly, it’s written using v6.3 of the AJAX control – v7.0 has been available for well over 6 months now and (despite some teething problems) this latest version is recommended for all new development.
  • Secondly, the article describes how to draw arbitrary Bezier curves on the projected plane of the map. Whilst this is an interesting exercise (and the author goes on to describe important concepts such as how to test the routine), it’s not actually that useful. More often, when we see curved lines on a map, we expect them to represent geodesics – the shortest path between two points on the surface of the earth. Although this was never the intention of the article, Bing Maps evangelist Chris Pendleton mistakenly drew this conclusion and tweeted a link to the article stating that it demonstrated geodesics, when in fact it does not.

Therefore, I thought that responding to this article would provide a good prompt for me to dust off and update my own v6.x geodesic curve script from several years ago (originally published here).

What’s a Geodesic, anyway?

A geodesic is a “locally-length minimising curve” (Mathworld) – it’s the shortest path between any two points on a given surface. On a flat plane, like a piece of paper, a geodesic is a straight line. On a sphere, a geodesic is a great circle. When dealing with geospatial data, a geodesic is the shortest distance between two points on the surface of the earth.

Generally speaking, Bing Maps has no regard for geodesic shapes relative to the earth’s surface – instead it draws shapes directly onto the projected map image. Drawing a straight line between two points on a map represents the shortest path between those points in the projected plane of the map, but it generally does not represent the shortest path between those same two points on the surface of the earth.

For example, consider a polyline drawn between Munich and Seattle, both of which lie at a latitude of approximately 48 degrees. You can define a polyline connecting these two points as follows:

Microsoft.Maps.Polyline([
  new Microsoft.Maps.Location(48, 11.5),
  new Microsoft.Maps.Location(48, -122)]);

When displayed on the map, this polyline will follow a constant latitude between the two points, like this:image

However, this is certainly not the shortest route between Munich and Seattle. If you are unsure why, consider how this same line would appear when viewed on a 3-dimensional model of the earth. In the screenshot below, the line that follows a constant latitude of 48 degrees, as shown above, is plotted in red, while the geodesic line that represents the true shortest line connecting the destinations is shown in blue:

image

Notice how, rather than being parallel to the equator, the geodesic route goes much further north over the top of the UK, then over Iceland, Greenland, and much of Canada before turning south again into Seattle. You can try this yourself on a globe – the geodesic route is the path that a piece of string follows when held tight between two locations. (For those readers familiar with SQL Server, the red line above is equivalent to a route calculated using the geometry datatype, while the blue line is equivalent to using the geography datatype)

As shown above, the shortest “straight line” route on a map is not the shortest direct path between two points on a globe. Likewise, the shortest geodesic route between two locations on the globe does not generally correspond to a straight line on a map. This is why, when airline companies show maps illustrating the flightpaths to various destinations, the lines appear curved – because they’re representing the geodesic path on the surface of the earth, which, when projected onto a map, will generally not be straight lines:

image

Drawing Geodesic curves in Bing Maps

Geodesics are clearly very useful if you want to visualise the path of shortest distance between two points. So how do you go about drawing geodesic curves in Bing Maps? Well, Bing Maps does not support curved geometries directly, so instead we must approximate the shape of a geodesic curve by creating a polyline containing several small segments. Using a larger number of segments will make the polyline appear more smooth and more closely resemble the shape of the smooth curve, but will also increase its complexity. I find that using 32 segments is more than sufficient accuracy for most maps. We’ll call this value n.

var n = 32;

Then, we need to determine the overall extent of the route, which we’ll call d. The shortest distance between any two points on a sphere is the great circle distance. Assuming that the coordinates of the start and end points are (lat1, lon1) and (lat2, lon2) respectively, measured in Radians, then we can work out the great circle distance between them using the Haversine formula, as follows:

var d = 2 * asin(sqrt(pow((sin((lat1 - lat2) / 2)), 2) + cos(lat1) * cos(lat2) * pow((sin((lon1 - lon2) / 2)), 2)));

We then determine the coordinates of the endpoints of each segment along the geodesic path. If f is a value from 0 to 1, which represents the percentage of the route travelled from the start point (lat1,lon1) to the end point (lat2,lon2), then the latitude and longitude coordinates of the point that lies at f proportion of the route can be calculated as follows:

var A = sin((1 - f) * d) / sin(d);
var B = sin(f * d) / sin(d);

// Calculate 3D Cartesian coordinates of the point
var x = A * cos(lat1) * cos(lon1) + B * cos(lat2) * cos(lon2);
var y = A * cos(lat1) * sin(lon1) + B * cos(lat2) * sin(lon2);
var z = A * sin(lat1) + B * sin(lat2);

// Convert these to latitude/longitude
var lat = atan2(z, sqrt(pow(x, 2) + pow(y, 2)));
var lon = atan2(y, x);

By repeating the above with different values of f, (the number of repetitions set according to the number of segments in the line), we can construct an array of latitude and longitude coordinates at set intervals along the geodesic curve from which a polyline can be constructed.

The following code listing wraps this all together in a reusable function, ToGeodesic, that returns an array of points that approximate the geodesic path between the supplied polyline or polygon locations.

To demonstrate the use of the function, I’ve added two entity collections to the map. The simple layer acts a regular shape layer, containing polylines and polygons displayed in red. Whenever an entity is added to this collection, the entityAdded event handler fires, which converts the added entity into the equivalent geodesic shape and adds it to the geodesicShapeLayer, displayed in blue. By maintaining two layers you can continue to deal with shapes as per normal in the layer collection – the additional points needed to simulate the geodesic display of each shape only get added in the copy of the shape added to the geodesicShapeLayer. You may then, for example, choose not to display the non-geodesic layer and only use it as a method to manage shapes, while the geodesic layer is used to actually display them on the map.

  
  

Here’s the results, showing both the flat (red) and geodesic (blue) layers of a polyline and a polygon:

image

Google Maps Mashups 1

Climate Hot Map



The Union of Concerned Scientists has created this Google Map to show the probable effects of global warming around the world. The map is accompanied by a Climate Hot Map Scavenger Hunt, which if you complete successfully gives you a chance to win a trip for two to the Rio Cachoeira Natural Reserve in Brazil.

The map explores the effect of climate change on people, the environment, the oceans, ecosystems and the temperature. You can select to explore any of these categories on the map using the menu at the bottom of the map.

Planefinder.net



The ash cloud created by the eruption of the Puyehue volcano in Chile continues to cause huge disruption to plane flights in Australia. Real-time flight tracking website planefinder.net is using Google Maps to show the location of the ash cloud as it drifts around the southern hemisphere.

The ash cloud is predicted to linger over south-east Australia for some time, causing widespread disruption to flights in and out of Sydney and Melbourne. At the time of writing the planefinder.net map shows a few flights in and out of south-east Australian airports but nowhere near the flight activity that can be seen in and around Perth.

Mibazaar has created a Google Maps based application to explore where people are searching for a given keyword in Google Search.

In this demo of the application you can view where people are searching for different makes of Ford car around the world. For each make of car you can view the ten locations where the most people are searching for that Ford.

The map includes historical data so you can view how searches have changed over the years for each make of car.

Mibazaar – Google Trends – Ford

sailorsmap.com


SailorsMap is a Google Map designed to help boat owners find useful places nearby.

Marinas and local stores that may be useful are added to the map on the fly. If your browsing device supports GPS then SailorsMap is automatically positioned at your current location.

As well as displaying nearby points of information, found via Google Maps Search the map, displays the nautical anchorages of Croatia.

Censo2010


The Instituto Brasileiro de Geografia e Estatística has created an application that allows you to browse the results of the 2010 Brazilian census on a Google Map.

Using the application you can click on a census tract on the map and view demographic information collected in the census. The information includes the population, the percentage of men and women and the percentage of different age groups in the population.

Dotter Example – San Francisco Crime Map


This Google Map displays 5000 crimes in San Francisco almost instantly on a Google Map. The map was created with the Google Maps API and the Dotter.js, a javascript class that generates data URIs.

The crimes displayed were committed in San Francisco between the 25th April and 25th May 2011. What is really impressive about the map is how quickly the 5000 data points load on the map.

If you want to create your own super-fast map with thousands of data points then the Dotter.js class is available on Github

Quake-Catcher Network


Many laptops these days are built with accelerometers that are designed to protect your hard drive from damage. The accelerometer detects sudden movement and can switch the hard drive off so that the heads don’t crash on the platters.

The Quake-Catcher Network realised that they could create the world’s largest and densest earthquake monitoring system simply by using the data from accelerometers in the world’s laptop computers. The Quake-Catcher Network links participating laptops into a single coordinated network that can detect and analyze earthquakes faster and better than ever before.

QCN uses Google Maps to show the data collected from participating laptops and from participating desktop computers with USB sensors. The map also shows the latest USGS reported earthquakes.

Live Call Map – OnSIP


OnSIP, a provider of Voice over IP calls has released a real-time Google Map of calls made using its service.

The map makes nice use of the drop marker animation in the Google Maps v3 API. Each time an OnSIP customer makes or receives a call, a map marker is dropped on the live map, openly displaying call volume peak and trend information. A marker is dropped on the map every time an OnSIP customer makes or receives a call.

Map Channels


Map Channels, the popular Google Maps creation tool, now lets you add data to a map from Google Fusion Tables.

In the four years that Map Channels has been running over 20,000 maps have been created by its users. It has proved popular with casual map makers and with major news organisations., including Fox News and CBC.

You can create a Google Map with Map Channels using data from a KML, a Google Spreadsheet, a GeoRSS feed, tab delimited text and now with a Google Fusion Table. You can see an example of a Map Channels created map with data provided by a Fusion Table in this Wikipedia Events Map.

Postholer.com


Postholer.com has created a Google Map of some of America’s most interesting trails. The map includes the route of the Appalachian Trail, the Pacific Crest Rail, the Continental Divide Trail and many more.

When you select a trail its route is displayed on the map. You can then select to view waypoints, points of interest, parking, camping spots and photos along the trail from a drop-down menu.

As well as providing a Google Map of the trails Postholer.com also provides a full set of printable maps for each trail.

Ofcom – Broadband Speeds Map

Ofcom, the regulator of the UK communications industries, has created a Google Map to show the speed of broadband throughout the UK.

Each county in the UK has been ranked on how they score on four broadband metrics; average sync speed, percentage getting less than 2Mbit/s, superfast broadband availability and broadband take up. The map displays as a basic heat map with each county coloured to show how they perform overall on each metric.

The map confirms what you already probably suspected. If you want superfast broadband then you are more likely to be lucky if you live in a big city. If you live in the Outer Hebrides then you are probably going to have make do with superslow broadband.

 

 

Cross from Google Maps Mania

Google is Trying To Bring ERP Consumers To Big Spatial Data Sets

SAP is going deeper with its Google collaboration to help customers manage large data volumes. The companies are working to make big data more intuitive, with visual displays to help decision-makers act more quickly.
Specifically, SAP plans to enhance its business -analytics software with location-based data capabilities that let people interact with real-time information. The companies want to pair enterprise apps with consumer tools like Google Maps and Google Earth.

“The trend toward ‘big data’ is accelerating the need for geospatial visualization of data. An increasing amount of data is being tagged with location information,” said Jonathan Becher, executive vice president of marketing at SAP. “For many applications, humans can see information relationships and data trends more easily when they are shown with maps and other spatial visualizations than they can using rows and columns of numbers. This allows non-expert users to make more accurate decisions on data, unlocking business intelligence for a wider audience.”

SAP-Google in Action

The SAP-Google partnership aims to help bring corporate information to life with location-based intelligence, including Google’s interactive map, satellite and even street-level views. Practically speaking, this allows SAP customers to analyze their businesses in a geospatial context to understand the “where” of their information.

Bringing mapping and other real-time technologies to the big-data front also lets decision-makers identify global, regional and local trends and how different scenarios impact them. The intended result includes increasing efficiency and profitability. SAP offers several examples of how organizations running SAP solutions with Google Maps API Premier could benefit from overlaying enterprise information onto intuitive mapping tools.

For instance, a telecom operator could use Google Earth and SAP BusinessObjects Explorer software to perform dropped-call analysis and pinpoint the coordinates of faulty towers. A state revenue department could overlay household tax information on a map and group it at the county level to track the highest and lowest tax bases. Or a mortgage bank could perform risk assessment of its mortgage portfolio by overlaying foreclosure and default data with the location of loans on Google Maps.

“SAP is using a private API from Google that is not currently available to any other enterprise-software vendor. This private API provides additional functionality that, for example, allows the end user to upload their own geospatial information, including maps,” Becher said. “This opens up new use cases. A department store can use Google Street View to add an interactive virtual layout of their store with directions to each department. This street-view information can be combined with on-shelf availability and pricing information to allow customers to actually see the store and buy product from their mobile device.”
SAP is working to drive a convergence of enterprise and consumer software, giving an increasing number of people the ability to make important business decisions through the lens of mobile and social technologies while navigating the complexity of big data, or the growing volume, variety and increased velocity of information.

But do enterprises yet truly understand the value of this blend of enterprise with consumer technologies? “We are early on in the trends of marrying geospatial visualization tools with enterprise information, but it is rapidly accelerating,” Becher said. “We’ve seen dozens of use cases across all 24 SAP industries. Virtually every customer we’ve shared the vision with has been excited about using it to improve decision-making and operations in their business.”

newsfactor

Dynamic Animated Tile Layers in Bing Maps (AJAX v7) 3

Now that I’ve got a working animated tile class, it’s time to look and see what performance improvements I can make to it. As in my first post in this series, the frames of my animation are going to be built from weather RADAR data collected from the NOAA at regular intervals in time (every 15 minutes), over a 6 hour period. This means that, every time my animation runs through a complete cycle, I will have requested 24 times the number of tiles normally required to construct the background of the map display. The absolute key to making this solution usable was going to be in ensuring those tiles were as small filesize as possible.

Reducing the Bit Depth

My weather data was retrieved and the tile images constructed dynamically using a PHP script. Since I wanted my tiles to retain transparency (so that they could be overlaid on top of other layers), but didn’t want to use GIF format, my only choice was to opt for PNG files which can be created with the PHP imagepng function.

By default, PHP creates true colour (i.e. 32bit) PNG images. However, my NOAA radar imagery was only using a 256 colour palette so, as suggested by a commentator on one of my previous posts (thanks Chris!), I could save some space without affecting image quality by ensuring all my tile images were saved as 8bit PNG rather than 32bit PNG. To compare, here’s the RADAR image for tile quadkey 02311, recorded at 10:15am on 27th May 2011:

02311

32Bit PNG (192kb)

02311

8Bit PNG (9kb)

192kb reduced to 9kb with no change in appearance – that’s a pretty good start!

Compressing the Tiles

My next option was to compress the tiles. The PNG file format uses the Deflate compression algorithm, which has two key features: a.) It’s a lossless algorithm, so (unlike JPG) there is no reduction in image quality in a compressed image file, and b.) it has a static decompression rate. What that means is that, however much you compress a PNG file, it will not take longer to decompress. Unlike some archive formats, there is no trade-off between having a smaller compressed filesize, only for it to take longer to unpack or for it to be lower quality – with PNG, compression is always desirable. You can read a good introductory guide to PNG compression at http://optipng.sourceforge.net/pngtech/optipng.html

There are a number of different PNG compression tools out there. For comparison, I tried both an online service (PunyPNG), and a small downloadable Windows application that provides a front-end for a series of command-line PNG utilities (PNG Monster). Both are free to use, and don’t add any watermarks to your images.

PunyPNG compressed my 8bit PNG, original size 9,212 bytes, down to 7,270 bytes (a 22% reduction). PNG Monster did even better, reducing it to just 5,793 bytes (a 37% reduction).

02311

PunyPNG compressed 8Bit PNG image (8 bytes)

02311

PNG Monster compressed 8Bit PNG image (6 bytes)

What makes PNG Monster even more useful is that you can set it to batch process and compress all the PNG files in a directory, rather than having to upload them one by one to an online webservice. The interface isn’t much to look at (except for the nice icon of the Cookie Monster), but if it gets the job done, who cares?!

I set it to work in a directory that contained 256 tiles representing the tiles for one frame of animation, at zoom levels 5, 6, and 7. This reduced the total size from 1.2Mb down to 566Kb – less than half the original size:

image

The Only Way is… PNG?

(If you’re not au fait with pop singles from the late 1980s, that title was meant to be an homage to Yazz. Oh, never mind…).

I was pretty pleased with the results from PNG monster, but there was just one more option I wanted to explore. My reason for choosing PNG was because it supported transparency, and could therefore be added as a separate tilelayer overlaid on top of other background layers within the AJAX control. For example, you could place the animated weather tilelayers on top of the base aerial imagery tiles, or the road map tiles:

Base map style Transparent PNG Result
+ =
+ =

But what about, rather than creating the RADAR image as a transparent layer, I created an opaque tile that showed the data ready overlaid onto a Bing Maps imagery tile? That way, I could save the tile as a JPG file instead and take advantage of its (potentially) greater compression ratio.

The aerial image tile shown above, http://ecn.t1.tiles.virtualearth.net/tiles/h02311.jpeg?g=685&mkt=en-gb&n=z, is 24,128 bytes. Setting a compression ratio of 65%, I could create a JPG tile of almost exactly the same filesize as the original Bing Maps tile, but incorporated the radar image as well:

While, at first, I thought that embedding the RADAR imagery onto the background tile image might have been a good idea, (and, it would be, if I was only trying to display a single static tile combining weather data and imagery), the end JPG tile ended up being much greater size than my compressed PNG image tiles. In every frame of the animation, I’d be downloading the entire background map image for that tile again, and I’d also lose the flexibility of being to overlay a transparent layer on top of any other layer. Idea aborted – back to PNGs!


Dynamic Animated Tile Layers in Bing Maps (AJAX v7) 2

 

I considered various approaches that could be used to animate between tilelayers containing image tiles representing different frames of an animation. The approach I decided on was to buffer tilelayers onto an EntityCollection “queue”. The first entity in the collection would be the tilelayer of the currently displayed frame, and subsequent elements would be pushed onto the end to allow them to be pre-buffered by the time they were to be displayed on the map.

In this post, I’ll look at creating the various methods used to add and transition through the frames in the queue.

Variables

To start with, I defined some variables. The following public properties could be set to change the behaviour of the animation:

  • frames: the URL of the individual frames used to define the animation. This can be supplied in one of two ways:
    • As an array of image URLs, specified in frame order, e.g.:var frames = [“http://mapsys.info/wp-content/uploads/2011/05/9f93ac1bbeadkey.png.png”, “http://mapsys.info/wp-content/uploads/2011/05/5e69b74bbbadkey.png.png”, “http:///www.example.com/frame3/{quadkey}.png”];
    • As a single URL in which the supplied {frame} placeholder would be replaced with the frame number of the requested tile. e.g. var frames = “http://mapsys.info/wp-content/uploads/2011/05/0cf8dc8575adkey.png.png”;
  • loopbehaviour: What should happen when the animation continues beyond the last frame? I defined three possible options:
    • ‘loop’: animation loops back to the first frame and continues playing
    • ‘stop’: animation stops on the last frame
    • ‘bounce’: direction of animation changes and continues to play
  • framerate: The interval (in milliseconds) at which frames should be animated
  • opacity: The opacity at which the tiles should be displayed
  • lookAhead: The number of frames of animation that should be loaded in advance onto the tile queue.

And I also created the following internal variables to help with the mechanics:

  • intervalId: the intervalId assigned by setInterval() – used to start and cancel the animation
  • frame: The integer index used to keep track of the currently displayed frame
  • direction: The current direction of animation: 1 forwards, or –1 backwards

Functions

The basic public methods of my animated tile class would be straightforward, as follows:

  this.play = function() {
    _direction = 1;
    _play();
  }
  this.playbackwards = function() {
    _direction = -1;
    _play();
  }
  this.goToFrame = function(n) {
    _goToFrame(n);
  }
  this.stop = function() {
    _stop();
  }
  this.reset = function() {
    _reset();
  }

And the corresponding private methods that they called would be, for the most part, straightforward as well:

  /* Play the animation in the current direction */
  function _play() {
    if (_intervalId == "") {
      _intervalId = setInterval(_nextFrame, _options.framerate);
    }
  }

  /* Reset the animation back to the first frame */
  function _reset() {
    _animatedTileLayer.clear();
    _frame = 0;
    _redraw();
  }

  /* Stop the animation if currently playing */
  function _stop() {
    if (_intervalId != "") {
      clearInterval(_intervalId);
      _intervalId = "";
    }
  }

  /* Jump to the specified frame index */
  function _goToFrame(n) {
    if (n > 0 && (n < _frames.length || _frames.length == 1)) {
      _animatedTileLayer.clear();
      _frame = n;
      _redraw();
    }
  }

The two functions that actually do the grunt work are _nextFrame(), which is the method called repeatedly by setInterval() when the animation is playing, and _redraw(), which is the method that actually deals with the tilelayers on the queue. Here’s _nextFrame(), which is responsible for determining the next frame to queue up in the currently playing animation, taking account of specified behaviour when the last frame is reached:

function _nextFrame() {

  // Increment (or decrement) the frame counter based on animation direction
  _frame += _direction;

  // Test if requested frame lies outside specified array of frames
  if (_frames.length > 1 && (_frame >= _frames.length || _frame < 0)) {

    // Varies depending on desired loop behaviour
    switch (_options.loopbehaviour) {

      // Loop (default) the animation from the other end
      case 'loop':
        _frame = _frames.length - (_direction * _frame);
        break;

      // Stop the animation
      case 'stop':
        _stop();
        _frame -= _direction;
        break;

      // Continue by reversing direction of animation
      case 'bounce':
       _direction *= -1;
       _frame = _frame + (2 * _direction);
       break;
      }
    }
    // Push the next frame onto the queue
    _redraw();
  }

And here’s version 1 of _redraw(), which removes the currently displayed frame of animation, displays (at full opacity) the next frame on the queue, and ensures that the queue maintains the specified number of tilelayers to preload in advance:

  function _redraw() {

    // Retrieve the URI of the next frame
    var uri = "";
    if (_frames.length > 1) { // Specified array of frames
      uri = _frames[_frame];
    }
    else {
      uri = _frames[0].replace('{frame}', _frame); // Single URL with {frame} placeholder
    }

    // Create a new tilelayer for the requested frame
    // Visibility must be set to true and the tilelayer must have non-zero opacity
    // in order for tiles to be requested
    var tileOptions = {
      mercator: new Microsoft.Maps.TileSource({ uriConstructor: uri }),
      opacity: 0.01,
      visible: true,
      zIndex: 25
    };

    // Add the tilelayer onto the end of the tile queue
    var tileLayer = new Microsoft.Maps.TileLayer(tileOptions);
    _animatedTileLayer.push(tileLayer);

    // If there is only one frame of animation in the queue, make it visible
    if (_animatedTileLayer.getLength() == 1) {
      _animatedTileLayer.get(0).setOptions({ opacity: _options.opacity });
    }
    // Ensure the tile queue maintains specified length
    else while (_animatedTileLayer.getLength() > _options.lookAhead + 1) {

      // Set the opacity of the next frame to full
      _animatedTileLayer.get(1).setOptions({ opacity: _options.opacity });

      // Remove the currently displayed frame
      _animatedTileLayer.removeAt(0);
    }
  }

First Impressions…

Testing out my library for the first time, I was:
a.) surprised to find that it did actually work!, but at the same time…
b.) disappointed that there was a really annoying flicker between each frame change, even though the tiles for the next frame had been fully cached via the tile queue.

Even though my _redraw() function was setting the opacity to full on the next tile layer to be shown (i.e. the tilelayer at position 1 in my entitycollection) before removing the current tilelayer (the tilelayer at position 0), I found that the API was not firing these actions synchronously. Investigating some more, I found that somebody else had already noticed the same problem and had proposed a workaround – rather than use the setOptions() method of the API, I could set the opacity of the layers directly through editing the styles attached to the DOM element.

This approach is a bit risky because, as the other commentator notes, there doesn’t seem to be a reliable way to identify any particular tile layer (or other Bing Map entity) through the DOM – the Bing Maps v7 elements are remarkably lacking in useful things like unique IDs or Classes, so you can only target them by their position. If the animated tile layer is the first entity to be added to the map, I could seem to target it reliably using Map.getModeLayer().children[0].children[1] but I couldn’t guarantee this would always work. Hence, I decided to introduce a new option – to either run the animation in “safe mode” (which used only supported methods of the API) or “dangerous mode”, which achieved a smoother result by transitioning between frames directly through the DOM, but might break at any moment (exciting, huh?).

The required modification to the _redraw() method is as follows:

...
    // If there is more than one frame
    else while (_animatedTileLayer.getLength() > _options.lookAhead + 1) {

      // Display the next frame depending on the mode selected
      switch (_options.mode) {

        case 'safe':
          // Set the opacity using the API method - incurs slight delay
          _animatedTileLayer.get(1).setOptions({ opacity: _options.opacity });
          break;

        case 'dangerous':
          // Can reduce flicker by setting opacity directly through the DOM

          _map.getModeLayer().children[0].children[1].style.opacity = _options.opacity;
          break;
      }
...

Now that I’ve got the basic code working, I’m planning to see what I can do to improve performance.