Creating a rotating globe

The Orthographic projection displays the Earth like a 3D object, but it only shows us one side at a time, and only the center is shown accurately. In this section, we will use this projection and the zoom behavior to allow the user to explore the features by rotating and zooming in on the globe.

The code of this example is available in the chapter11/02-rotating file of the code bundle. We will begin by drawing a globe using the Orthographic projection. As we did in the previous section, we load the TopoJSON data and construct the GeoJSON feature collection that represents the ne_50m_land object:

d3.json('/chapter11/data/land.json', function(error, data) {

    // Handle errors getting or parsing the data
    if (error) { console.error(error); }

    // Construct the GeoJSON feature collection using TopoJSON
    var geojson = topojson.feature(data, data.objects.ne_50m_land);

    // Create the svg container...
});

We set the width and height of the svg element and use these dimensions to create and configure an instance of the Orthographic projection. We also select the container div and append the svg container to the map:

    // Width and height of the svg element
    var width = 600, 
        height = 300;

    // Create an instance of the Orthographic projection
    var orthographic = d3.geo.orthographic()
        .scale(height / 2)
        .translate([width / 2, height / 2])
        .clipAngle(90);

    // Append the svg container and set its size
    var div = d3.select('#map-orthographic'),
        svg = div.append('svg')
            .attr('width', width)
            .attr('height', height);

We create an instance of the geographic path generator and set its projection to be the Orthographic projection instance:

    // Create and configure the geographic path generator
    var path = d3.geo.path()
        .projection(orthographic);

We will add features to represent the globe, the land, and lines for the parallels and meridians. We will add a feature to represent the globe in order to have a background for the features. The path generator supports an object of the Sphere type. An object of this type doesn't have coordinates, since it represents the complete globe. We will also append the GeoJSON object that contains the land features:

    // Globe
    var globe = svg.append('path').datum({type: 'Sphere'})
        .attr('class', 'globe')
        .attr('d', path);

    // Features
    var land = svg.append('path').datum(geojson)
        .attr('class', 'land')
        .attr('d', path);

We will also add the graticule, which is a set of parallels and meridian lines, in order to give us a more accurate reference for the orientation and rotation of the sphere:

    // Create the graticule generator
    var graticule = d3.geo.graticule();

    // Append the parallel and meridian lines
    var lines = svg.append('path').datum(graticule())
        .attr('class', 'graticule')
        .attr('d', path);

The preceding code will show us the Earth with the same aspect as the previous section. The strategy to add rotation and zoom to the globe will be to add an invisible overlay over the globe and add listeners for the pan and zoom gestures using the zoom behavior. The callback for the zoom event will update the projection rotation and scale and update the paths of the features using the path generator configured earlier. To keep the state of the zoom behavior and the projection in sync, we will store the current rotation angles and the scale of the projection in the state variable:

    // Store the rotation and scale of the projection
    var state = {x: 0, y: -45, scale: height / 2};

We will update the configuration of the projection to use the attributes of this variable, just for consistency:

    // Create and configure the Orthographic projection
    var orthographic = d3.geo.orthographic()
        .scale(state.scale)
        .translate([width / 2, height / 2])
        .clipAngle(90)
        .rotate([state.x, state.y]);

The zoom and pan should be triggered only when the user performs these gestures over the globe, not outside. We will create an overlay circle of the same size as the globe and set its fill-opacity attribute to zero. We will bind the state variable to the overlay in order to modify it in the zoom callback:

    // Append the overlay and set its attributes
    var overlay = svg.append('circle').datum(state)
        .attr('r', height / 2)
        .attr('transform', function() {
            return 'translate(' + [width / 2, height / 2] + ')';
        })
        .attr('fill-opacity', 0);

We need to create an instance of the zoom behavior and bind it to the overlay. This will add event listeners for the pan and zoom gestures to the overlay. We will limit the scale extent to between 0.5 and 8:

    // Create and configure the zoom behavior
    var zoomBehavior = d3.behavior.zoom()
        .scaleExtent([0.5, 8])
        .on('zoom', zoom);

    // Add event listeners for the zoom gestures to the overlay
    overlay.call(zoomBehavior);

When the user zooms or pans the overlay, a zoom event is triggered. The current event is stored in the d3.event variable. Each event type has its own attributes. In the case of the zoom event, the zoom translation vector and the scale are accessible through the d3.event.translate and d3.event.scale attributes. We need to transform the scale and the translation vector to the appropriate projection scale and rotation:

function zoom(d) {

    // Compute the projection scale and the constant
    var scale = d3.event.scale,
        dx = d3.event.translate[0],
        dy = d3.event.translate[1];

    // Maps the translation vector to rotation angles...
}

The zoom event will be triggered several times when the user performs the drag gesture. The translate array accumulates the horizontal and vertical translations from the point where the drag gesture originated. If the user drags the globe from the left-hand side to the right-hand side, the globe should be rotated 180 degrees, counterclockwise. We will map the horizontal position of the translation vector to an angle between 0 and 180 degrees:

    // Maps the translation vector to rotation angles
    d.x = 180 / width * dx;    // Horizontal rotation
    d.y = -180 / height * dy;  // Vertical rotation

If the user drags the North Pole towards the bottom of the image, we will want the globe to rotate forward. As the latitude is measured from the equator to the poles, we need to rotate the projection by a negative angle in order to have a rotation forward from the point of view of the observer. With the rotation angle computed, we can update the projection's rotate and scale attributes. The zoom scale is a relative zoom factor, and we need to multiply it by the original size of the map to obtain the updated scale of the projection:

    // Update the projection with the new rotation and scale
    orthographic
        .rotate([d.x, d.y])
        .scale(scale * d.scale);

To update the image, we need to reproject the features and compute the svg paths. The path has a reference to the projection instance, so we need to just update all the paths in the svg with the updated projection:

    // Recompute the paths and the overlay radius
    svg.selectAll('path').attr('d', path);
    overlay.attr('r', scale * height / 2);

We also updated the overlay radius so that the dragging area coincides with the globe, even if the user changes the zoom level. The globe can be rotated and zoomed as shown in the following screenshot:

Creating a rotating globe

The globe can be rotated and zoomed with mouse or touch gestures, allowing the user to explore every part of the globe in detail. The strategy to add rotation and zoom behaviors can be used with any projection, adapting the mapping between the zoom translation and scale to rotations and scale of the projection. Depending on the level of detail of the features, there can be some performance issues during the rotation. The projection and path generation are being done on each rotation step. This can be avoided by rendering simpler features during the rotation, or even showing just the graticule when rotating and rendering the features when the user releases the mouse.

The rotation of the globe is not perfect, but it's a good approximation of what we would expect. For a better (but more complex) strategy, see the excellent article from Jason Davies at https://www.jasondavies.com/maps/rotate/.

In the next section, we will use the Stereographic projection and the zoom behavior to create an interactive star map.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset