Google Earth: “The Immortal Game”

 

1851 was the year of the Great Exhibition in London (England not Ontario). The chess community marked the event by staging the first international chess tournament which brought in the best players of Europe.

An exciting game played during the event became tagged ‘The Immortal Game’. The opponents were Adolf Anderssen, the eventual winner of the tournament, and Lionel Kieseritsky. If Pyrrhus, son of Achilles, had not already given his name to a type of battle in which victory was gained despite incurring heavy losses, then we might today have used the name of Anderssen with the same meaning, for he sacrificed many pieces to get one bishop and two knights into a winning position.

 

chess.jpg 

Anderssen was much the more aggressive player but the rate of attrition was high. He lost a pawn early on when his King’s Gambit was accepted. Then a bishop fell, and another pawn, then both castles. Finally, he forced checkmate by sacrificing his queen even while Kieseritsky thought he was doing well to have Anderssen’s king on the run as his queen and a bishop controlled row 1 of the board.

So, why the animation in Google Earth? Colin said:

“I imagined the game as a battle between two armies, that’s easy enough to do, but I also wanted to tell a story. As well as being fun to create, the animation is intended to illustrate not only the chess game but also a story in which an embedded reporter watches the raging battle and the bloodshed all around from the position of the King’s Pawn. I’ve written a short story which now forms the bones of a longer novel.

It took a while to find the right place on Earth to set the battle. I needed high ground as defensive positions for Kieseritsky and a landscape that would match the story. I found this in the mountains and valleys of the English Lake District, one of my favourite places and destination of many camping trips.

I created the chess pieces using Google SketchUp and that was a fun exercise in itself. I exported the pieces from SketchUp as 3D models which I could then place and animate in Google Earth. The latest version uses the gx:Tour features of KML; previously I had animated the game using set positions after each move, but that doesn’t flow so well. I’m pleased with the way the pieces now glide over the ground.”

You can view it yourself in Google Earth by using this KML file.

Creating Dynamic Animated Tile Layers in Bing Maps 4

 

If you’ve been following this series of blog posts, you’ll know that I’ve been investigating methods to create an animated background tile layer to display weather maps using the Bing Maps v7 AJAX control.

For this final post, I’m going to wrap everything I’ve done so far into a reusable module.

Bing Maps Custom Modules and the “Ninja” update

The most recent update to the Bing Maps AJAX control, introduced in early May 2011, became slightly notorious as the “ninja” update. The reason being that, silent but deadly, the unannounced update accidentally broke many websites with no warning. Unfortunately, these breaking changes rather eclipsed some of the other nice new features introduced in the update – one of which was the introduction of a common framework through which Bing Maps could be extended by creating custom modules.

Now that the bugs introduced in the latest update have mostly been ironed out, I thought it time to investigate the new module functionality. It’s pretty easy to move your custom code into a module – the main requirement is that, at the end of your function, you place a call to the Microsoft.Maps.moduleLoaded() method. This lets Bing Maps know that your module had been loaded and ready to use.

So, I added the moduleLoaded() call, rolled my animatedTileLayer function into a file and uploaded it to http://www.a3uk.com/bm_modules/animatedtilelayer.js. Note that custom modules registered via the Bing Maps registerModule() method must be placed on a publicly-accessible website – you can’t register a custom module on a local URL. So, when you’re developing a module locally, I recommend that you get it working properly first using a regular embedded script, and then wrap it in a module at the last minute.

To use my new module, I registered and loaded it as follows:

// Register and load the animation module
Microsoft.Maps.registerModule('animatedTileLayerModule', 'http://www.a3uk.com/bm_modules/animatedtilelayer.js');
Microsoft.Maps.loadModule("animatedTileLayerModule", { callback: animationModuleLoaded });

The animationModuleLoaded callback would be fired after the module had loaded, and was responsible for creating a new instance of the animatedTileLayer class and adding it to the map, as follows:

function animationModuleLoaded() {

// Define the array of frame URLs. The URL for the tiles of each frame are
// stored in subdirectories corresponding to each timestamp, named by quadkey
var frames = [
  'http://www.a3uk.com/demos/aniamtedtilelayer/tiles/201105271015/{quadkey}.png',
  'http://www.a3uk.com/demos/aniamtedtilelayer/tiles/201105271030/{quadkey}.png',
  'http://www.a3uk.com/demos/aniamtedtilelayer/tiles/201105271045/{quadkey}.png',
  ...
];

// Create a new instance of the AnimatedTileLayer class
animatedTileLayer = new AnimatedTileLayer(
  map,     // Map instance to which tilelayer should be added
  frames,  // Array of frames of the animation
  {
     framerate: 1000,        // Delay between frame changes (ms)
     loopbehaviour: 'loop',  // Behaviour when last frame is met
     opacity: 1,             // Opacity of tile layer
     mode: 'dangerous',      // Method to use when switching layers
     frameChangeCallback: updateFrameCounter     // Callback on every frame change
   }
);

// Add the tilelayer to the map
map.entities.push(animatedTileLayer);
}

(The options passed to the AnimatedTileLayer constructor are explained in my previous posts). I also added a couple of basic HTML buttons to the page that would control starting, stopping, and resetting the animation, and a text input box that would display the timestamp of the currently displayed frame:

Animation Controls
 

The current frame counter would be updated by the function specified in the frameChangeCallback:

// Callback gets called every time frame changes
function updateFrameCounter(n) {

  // Display the frame counter
  document.getElementById('frameIndex').value = n;

  // Show the timestamp corresponding to this frame
  var t = String(timeStamps[n]);
  document.getElementById('frameTimeStamp').value = t.substr(0, 4) + '-' + t.substr(4, 2) + '-' + t.substr(6, 2) + ' ' + t.substr(8, 2) + ':' + t.substr(10,2);
}

Put it all together and what have you got?

So that’s it – my animatedtilelayer class wrapped into a custom module, registered and loaded from a page that creates a new animated tile layer and calls into a couple of the methods provided by the class.

You can play with (version 1.0) of the final product at http://www.a3uk.com/demos/animatedtilelayer/:

image

To Do:

There’s still a few bugs that I’m aware of that, at some point, I might try to fix:

  • Memory usage (especially in Firefox) seems to be an issue, and grows substantially if the animation is left running for a while – I’m not sure memory from the old tile layers is properly released following entitycollection.clear(), but I haven’t investigated this fully yet. (This problem is also reported by another user here). IE9 and Chrome appear to fare much better.
  • The tile layer vanishes while the map is scrolled (but then reappears when the map has finished scrolling) – this doesn’t seem to be unique to my animated tile layer, but is a behaviour of the way Bing Maps handles custom tile layers in general, so I’m not sure there is anything I can do about it.
  • By necessity of my attempt to pre-load the next tilelayer before displaying it on the map (to enable smoother transitions between frames), there is an initial delay after clicking “Play” before the first frame change. Again, I think this might end up being marked as “by design”!
  • I’m sure there’s more – I haven’t exactly comprehensively QA’d this module, but you’re free to make use of it as you see fit.

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.