Thursday, July 11, 2013

Converting from Leaflet to Bing Maps

A few months ago I developed a map-based game for a contest. It was a simple multiplayer wargame on which 4 players could try to conquer Europe using a bunch of different units.


For the mapping API I had to decide between Bing Maps and Leaflet. At the time, as I expected to have to use Canvas to draw the hexagons (which Leaflet supports with canvas tile-layers) I chose Leaflet.

I've now decided to pick-up this game and evolve it into something more "tangible". Thus, and although Leaflet has nothing wrong by itself, it's "just" a map api. I need something more complete and so I've decided to port the game to Bing Maps. Afterwards I'll improve it with some really cool ideas that I have. This blog post should be more or less a catalog of the changes I had to make to convert this game from Leaflet to Bing Maps.


First of all let me start by showing you both versions:
They should behave quite similarly performance-wise although the inertia feeling with Bing Maps is top-notch and hard to beat.

The game includes a tutorial and should be easy enough to grasp. Just open two or more tabs on the browser to simulate different players (I've tested it in Chrome and IE 10)

The map itself looks like this:


Basically all that image (except those 4 colored hexagons) is composed of server-tiles generated by me (using C# to create the hexagons and TileMill to design/generate the tiles).
  • Loading the map without base images
Leaflet, as it doesn't provide data by itself, doesn't show any tiles by default. As I want to have my own base images the Bing Map should be loaded with the following attribute:
mapTypeId: Microsoft.Maps.MapTypeId.mercator

That one was easy :)

  • Limiting the Zoom Levels
Now this was slightly more tricky. I only generated tiles for zoom levels between 6 and 8, mostly because the game wouldn't be very playable outside these levels, as the units would be too small/large to be controlled properly.

In leaflet you just pass a "maxzoom" and "minzoom" attributes to the map constructor and you're set. With Bing one needs to be more creative, namely handling the "viewchangestart" to prevent crossing the zoom boundaries, like this:
MM.Events.addHandler(gameData.map,'viewchangestart',function () {
    if (gameData.map.getZoom() <= gameData.map.getZoomRange().min) {
        gameData.map.setView({
            'zoom':       gameData.map.getZoomRange().min,
            'animate':    false
        });
    }
    else if (gameData.map.getZoom() >= gameData.map.getZoomRange().max) {
        gameData.map.setView({
            'zoom':       gameData.map.getZoomRange().max,
            'animate':    false
        });
    }
});
  • Loading the hexagons tile-layer
Another interesting detail about the generated tiles is that they're in zxy format, particularly in TMS format. Bing Maps uses quadkeys by default to identity the tiles, which is a pretty different beast. Fortunately the Bing Maps developers were kind enough to provide lots of extensibility points in the API, thus making possible to do some code like this (which I blatantly stole from an Alastair Aitchison post):
var tileSource = new MM.TileSource({ 
    uriConstructor:  function getTilePath(tile) {
        var x = tile.x;
        var z = tile.levelOfDetail;
        var yMax = 1 << z;
        var y = yMax - tile.y - 1;

        return "Images/Tiles/BaseMap4/" + z + "/" + x + "/" + y + ".png";
}});
var tileLayer = new MM.TileLayer({ mercator: tileSource, opacity: 1 });

Basically one has to define a TileSource with an implementation on how to build the tile's urls. Seems complex but it's a pretty robust mechanism.
  • Image Overlays
Those 4 colored hexagons are not in the base maps, nor are pushpins. They're actually image overlays which blend seamlessly with the base map.


Bing Maps doesn't support this out-of-the-box but fortunately Ricky has created a brilliant Bing Maps module that does exactly that. So:

Leaflet code:
var overlay = L.imageOverlay('Images/Units/base_p' + (i+1) + '.png',
                            [[maxLat, minLon], [minLat, maxLon]])
                            .addTo(gameData.map);

Bing Maps code (after loading the module):
var imageRect = MM.LocationRect.fromCorners(
    new MM.Location(maxLat, minLon), new MM.Location(minLat, maxLon));
var overlay = ImageOverlay(gameData.map, 
    'Images/Units/base_p' + (i+1) + '.png', imageRect));
  • Handling Events on units
Event-handling is one of the most tricky parts in this game. Lots of events are handled on the units depending on their current state:
    • click
    • dragstart
    • drag
    • dragend
The APIs differ a little bit on this but are similar enough (the event names are actually the same). Leaflet uses a "fluent" like syntax and Bing Maps a more conventional approach, but both work similarly:

Leaflet:
var marker = L.marker(hexagon.center, {...})
      .on('dragstart', function(e) {...})
      .on('drag', function(e) {...})
      .on('dragend', function(e) {...})

Bing Maps:
MM.Events.addHandler(pushpin, 'dragstart', function(e){...});
MM.Events.addHandler(pushpin, 'drag', function(e){...});
MM.Events.addHandler(pushpin, 'dragend', function(e){...});

What was a little bit more cumbersome is removing event handlers from pushpins. The "removeHandler" function receives the "handler ID", which isn't typically that practical to hold. I implemented a workaround using a "private" field from the Pushpin object:
if(MM.Events.hasHandler(pushpin, 'click')) {
   MM.Events.removeHandler(marker.mm$events.click[0]);
}
I'm not terrible happy with that solution but it does work fine.
  • PolygonCollection vs EntityCollection
Well, in Leaflet there's a PolygonCollection and in BingMaps an EntityCollection. They're pretty similar and were, by far, the easiest objects to convert from Leaflet to Bing Maps.

  • HTML Content in Markers/Pushpins
I use HTML content for the descriptive dynamic text around the bases.



Both Leaflet and Bing Maps support HTML content in the pushpins. Here's how they're declared (I've omitted the non-relevant fields):

Leaflet:
var info = L.divIcon({
 className: 'base-text',
 html: html,
 iconSize: [150, 50],
 iconAnchor: [150, 0],
});

Bing Maps:
var info = {
        htmlContent: html,
        anchor:  new MM.Point( 150, 0),
        width: 150,
        height: 50,
        typeName: 'base-text'
};

The "className"/"typeName" attributes map to a css class that should be defined in your styles file.

  • Modifying Units Opacity
When a unit has been moved it passes through a cool-down time on which it has to wait before moving again. During this small period the unit is displayed as transparent.

In Leaflet this is done directly with a .setOpacity function. With Bing Maps this is done by changing the css class of the element:
marker.setOptions({typeName: 'unit-disabled'});
And on the css file:
.unit-disabled { opacity: 0.25; }

  • Other notable differences
Handling polylines, polygons, coordinates, colors is also pretty different. Just look at the code as it should be pretty self-explanatory. It's neither minified nor obfuscated (and also not polished :D).


To be continued...

Basically both APIs have chosen a different approach on most stuff, making the conversion non-trivial for more complex scenarios (like this one). Anyway, I'm making the Bing Maps version the "official" one, and anxious to start implementing my new ideas.

4 comments:

  1. Great App ... ! Congrats on the award

    ReplyDelete
  2. Hi, how long did it take you to do the whole migration incl the tweaks on the UI?

    ReplyDelete
  3. You've done an amazing job. Where can I find your game?

    ReplyDelete