A fancy product comes always with tricky features. Implementing the interactive 3D map view for the Flugrouten-Radar has been a chance to deal with new techniques involving Javascript, CSS 3D Transforms, map projections and more. Let’s walk through gotchas and solutions.

From prototype to features

Luckily enough, we didn’t have to start from scratch.

ProPubblica, who designed the first prototype, provided us with their source code. Despite being an initial implementation, it was already addressing the basic functions. Here it is a comparison of the 3d view prototype vs the final version:

To our surprise, the 3d effect was done entirely via CSS3. Each trace was a canvas created using Raphaël, while the bottom ground was created via the Google Maps static service.
It came with an automatic isometric rotation feature, rotating the view on mouse move, independently of the mouse position on the screen. More important, most of the projection code needed for generating the SVG canvas has been already written and put in places. We just had to generalize it, fix it a bit and incorporate it as a helper library, allowing us to get vector lines with the correct projection (Mercator), size and zoom level based on simple coordinates.

How it works

The prototype looked fancy, yet incredibly easy in its principle.
First, stack absolutely positioned layer one on top of each other. This is as simple as:

3D Transforms magic

3D Transforms allow you to manipulate DOM elements like planes in a tridimensional space. As most CSS properties, those are ihnerited by an element’s children, which means that for our purpose it is enough to rotate the container:

.asf-3d-chart-container {
// …
@include transform(rotateX(57deg) translateZ(-80px));
// Rotates the map and pushes it to the bottom of the container
@include transform-origin((center center 0));
// Specifies the pivot for the transform to be at the bottom-center
}

That was easy. Now it’s time to space the lines vertically.
This is done dynamically in our Backbone-view during plotting. The complete process of rendering the SVG is a bit complicated to be explained briefly. For now is enough to know that at some point something like this happens:

Where $layerContainer is one of our jQuery-wrapped divs with class asf-3d-map-layer and layerHeight is a normalized measurement in pixel calculated upon the height in meter of the trace relative to the max-height we want to achieve. This ensures that each layer has an appropriate Z translation value, e.g. translate3d(0,0,20px).
However in order to see the effect, we need to add an additional CSS-property to the container:

preserve-3d is absolutely necessary for the Z spacing to be rendered correctly. What it does, is specifying that the DOM element is by all means a 3d projected viewport. If not specified, Z distances will be ignored.

IE Note: at the time of writing, preserve-3d is still not supported by any version of IE explorer (<= 10)

This is the result:

We are close! But as in the prototype, this is still isometric, let’s add perspective:

The effect cannot be appreciated sensibly on a still image but it looks much more natural during rotation. In order to make the effect visible here it’s a screenshot blended with the isometric version:

Additional details

Once grasped the concept, adding other elements is easy. The original prototype had a round map tile, but given the designs provided by Morgenpost, we needed to add a bigger, fading tile on the ground. A sprite-like (always showing the same face) pin in the center and legend on the side were also needed. So we expanded our previous markup to include the new elements, looking like this:

asf-3d-background is a container for all the elements constituing the ground of the view: the background map tile image, the nord-indicator and the area circle

asf-pin-container will house the pin pointer itself. The container will be needed for the sprite effect

asf-legend-container will contain the altitude label sprites.

Map tile

While Google Maps was a quick and efficient solution for the prototype, it couldn’t be used commercially for this kind of application. So we switched to MapBox for all needed map issues.
For the ground map image we’re using MapBox tiles service. The service itself allows to request a static image centered in one point with specific coordinates and at a certain zoom level:

The URL is built in the template straight from the current selection model used by the view, via the coordinates mapBoxCoords and the zoom mapZoom.

640×640 is the maximum size provided by the service. The tricky point is that we need to cover a bigger area than the selection itself and the image needs to match the scaling of the traces, but 640×640 is too small, considering that our analysis area is 380px wide. As both the traces use the Spherical Mercator projection and same tile system as MapBox does, and keeping in mind that the area covered by a tile just doubles in size by reducing the zoom level by 1, we can get a larger but still in proportion image by simply reducing the zoom level by 1 compared to our traces. We then double the size of the container, keeping the covered area in proportion.

In the URL we asked for a zoom 13, meaning that traces’ zoom level will be 14. Knowing this fact, and knowing the original tile size (see above) we can calculate the size and the offset that the background image should have: .

NOTE: to be fair, the size of the tile would need to be 760x760px, exactly double the size of traces’ container size. This is indeed a workaround for a proportion issue within the projector which we’re working out for the next update.

In order to fade out the background a bit, and reduce the ‘squared’ look, we used the :after pseudo-class to create an overlay mask with a radial gradient, transparent in the middle and white at the borders:

Pin

This is one of the two ‘billboard’ elements we needed. We have to position it in the middle of our tile but we also need to rotate it in order to let it face the user and not just laying flat on the ground:

The useful fact here is that that container has not been rotated yet. This means that when rotating the rest of the view we can just rotate the pin container in the opposite direction, in order to mantain the ‘sprite’ look of the pin. We’ll see that later.

Legend

This is a bit trickier. The legend should work as a sprite, but it should be laying at the side of the circle. This is simply solved by moving the container on the side of the circle, and applying an inverse Z rotation to its content afterwards.
We first extend our markup to be like the following:

(the label color is assigned by Javascript, to match the same used by the traces)

Drag&Drop 3D Rotation

When the user drags the map, we need to pay attention to the billboard elements, so to keep them facing the user. We can accomplish this by rotating the sprite elements by the same amount of degrees, but in the opposite direction. For this purpose we defined this function in our view:

Initial rotation

This was trickier, because jQuery.animate() cannot act on non-numeric properties such as trasform. The workaround to this, was to animate an unused parameters (border-spacing) and use the step function to rotate the map based on the interpolated value: