Projecting raster images with D3

Until now, we have used svg to create maps. In this section, we will learn how to use D3 to project raster images in canvas elements. This will allow us to use JPG or PNG images to generate orthographic views of these images. A raster image reprojected using the Orthographic projection is shown in the following screenshot:

Projecting raster images with D3

Rendering an image of Earth using the Orthographic projection (or any other projection) involves manipulating two projections. First, for each pixel in the original image, we compute its corresponding geographic coordinates using the image inverse projection. Then, we use the geographic coordinates of each pixel to render them using the Orthographic projection. Before beginning the implementation of these steps, we will discuss the inverse method of a projection.

Projections are functions that map geographic coordinates to points on the screen. The inverse projections do the reverse operation; they take coordinates on the two-dimensional surface and return geographic coordinates. In the chapter11/05-raster file, there is an interactive example that computes the geographic coordinates of the point under the mouse. Note that not all projections in D3 have an inverse operation. Computing the geographic coordinates of a point under the mouse is shown in the following screenshot:

Projecting raster images with D3

To obtain the geographic coordinates that correspond to a point under the cursor, we can add a callback to the mouseover event over an element:

    // Callback of the mouseover event
    rect.on('mousemove', function() {
        // Compute the mouse position and the corresponding
        // geographic coordinates.
        var pos = d3.mouse(this),
        coords = equirectangular.invert(pos);
     })

The Next Generation Blue Marble images are satellite images of Earth captured, processed, and shared by NASA. There are monthly images that show Earth with a resolution of 1 pixel every 500 meters. These images are available in several resolutions at the NASA Earth Observatory site at http://earthobservatory.nasa.gov/Features/BlueMarble/. We will use a low-resolution version of the Blue Marble image in this section.

Rendering the raster image with canvas

This time, we won't use svg to create our visualization, because we need to render individual pixels in the screen. Canvas is more suitable for this task. We will begin by creating a canvas element, loading the Blue Marble image, and drawing the image in the canvas element. We select a container div and append a canvas element, setting its width and height as follows:

// Canvas element width and height
var width = 600,
    height = 300;

// Append the canvas element to the container div
var div = d3.select('#canvas-image'),
    canvas = div.append('canvas')
        .attr('width', width)
        .attr('height', height);

The canvas element is just a container. To draw shapes, we need to get the 2D context. Remember that only the 2d context exists:

    // Get the 2D context of the canvas instance
    var context = canvas.node().getContext('2d'),

Then, we create an instance of Image. We set the image source and set a callback that can be be invoked when the image is fully loaded:

    // Create the image element
    var image = new Image;
    image.onload = onLoad;
    image.src = '/chapter11/data/world.jpg';

In the onLoad function, we use the canvas context to draw the image. The arguments of the drawImage method are the image, the offset, and size of the source image and the offset and size of the target image. In this case, the original image size is 5400 x 2700 pixels; the target image size is just 600 x 300 pixels:

    // Copy the image to the canvas context
    functiononLoad() {
        context.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
    }

The Blue Marble image rendered in a canvas element is shown in the following screenshot:

Rendering the raster image with canvas

As the Blue Marble image was created using the Equirectangular projection, we can create an instance of this projection and use the invert method to compute the longitude and latitude that corresponds to each pixel:

    // Create and configure the Equirectancular projection
    var equirectangular = d3.geo.equirectangular()
        .scale(width / (2 * Math.PI))
        .translate([width / 2, height / 2]);

Computing the geographic coordinates of each pixel

We can add an event listener for the mousemove event on the canvas element. The d3.mouse method returns the position of the mouse relative to its argument, in this case, the canvas element:

    // Add an event listener for the mousemove event
    canvas.on('mousemove', function(d) {

        // Retrieve the mouse position relative to the canvas
        var pos = d3.mouse(this);

        // Compute the coordinates of the current position
    });

We can use the invert method of the projection to compute the geographic coordinates of the point under the cursor. To display the geographic coordinates that correspond to the position of the cursor, we clear a small rectangle of the canvas content and add fillText to add the label in the upper-left corner of the image:

        // Compute the coordinates of the current position
        var coords = equirectangular.invert(pos);

        // Create a label string, showing the coordinates
        var label = [fmt(coords[0]), fmt(coords[1])].join(', '),

        // Cleans a small rectangle and append the label
        context.clearRect(2, 2, 90, 14);
        context.fillText(label, 4, 12);

Using the invert method to compute geographic coordinates is shown in the following screenshot:

Computing the geographic coordinates of each pixel

Reprojecting the image using the Orthographic projection

Until now, we have learned how to copy an image element in canvas and how to use the invert method of a projection to compute the geographic coordinates that correspond to a pixel in the canvas element. We will use this to reproject the raster image using the Orthographic projection instead of the original Equirectangular projection. The strategy to project the image into a different projection is as follows:

  • Insert the source image in the canvas element, setting its width and height.
  • Create an instance of the source projection (the projection used to generate the image) and configure it in a way that if it were used to project the world, it would fit exactly with the source image.
  • Create an empty target image. The size of this image should fit the target projection.
  • Create and configure an instance of the target projection. In this case, we will use an instance of the Orthographic projection.
  • For each pixel in the target image, use the invert method of the target projection to compute the geographic coordinates that correspond to that pixel. Using the source projection, compute the pixel coordinates of that location, and copy the pixel data to the pixel in the target image.

The procedure sounds a little convoluted, but it's basically about copying the pixels from the source image to the target image using the geographic coordinates in order to know where each pixel can be copied to.

We begin by drawing the source image in the canvas element once the image is fully loaded. Images in canvas are represented as arrays. We read the data array of the source data and created an empty target image and got its data:

// Copy the image to the canvas context
function onLoad() {

    // Copy the image to the canvas area
    context.drawImage(image, 0, 0, image.width, image.height);

    // Reads the source image data from the canvas context
    var sourceData = context.getImageData(0, 0, image.width, image.height).data;

    // Creates an empty target image and gets its data
    var target = context.createImageData(image.width, image.height),
    targetData = target.data;

    // ...
}

Note that the target image is not shown yet, but we will use it later. In canvas, images are not stored as matrices; they are stored as arrays. Each pixel has four elements in the array, which are its red, green, blue, and alpha components. The rows of the image are stored sequentially in the image array. With this structure, for an image of 200 x 100 pixels, the index of the red component of the pixel 23 x 12 will be 4 * (200 * 11 + 23) = 844.

To make things easier, we will iterate the image data as if it were a matrix, computing the index of each pixel with the aforementioned expression. We iterate in columns and rows of the target image and compute the corresponding coordinates of the current pixel using the invert method of the target projection:

// Iterate in the target image
for (var x = 0, w = image.width; x < w; x += 1) {
    for (var y = 0, h = image.height; y < h; y += 1) {

        // Compute the geographic coordinates of the current pixel
        var coords = orthographic.invert([x, y]);

        // ...
    }
}

The inverse projection could be undefined for a given pixel; we need to check this before we try to use it. We can now use the source projection to compute the pixel coordinates of the current location in the source image. This is the pixel that we need to copy to the current pixel in the target image:

        // Source and target image indices
        var targetIndex, sourceIndex, pixels;

        // Check if the inverse projection is defined
        if ((!isNaN(coords[0])) && (!isNaN(coords[1]))) {

            // Compute the source pixel coordinates
            pixels = equirectangular(coords);

            // ...
        }

Knowing which source pixel we need to copy, we need to compute the corresponding index in the source and target image data arrays. The projection could have returned decimal numbers, so we will need to approximate them to integers. We will also ensure that the indices of the red channel for both images should be exactly divisible by four:

        // Compute the index of the red channel
        sourceIndex = 4 * (Math.floor(pixels[0]) + w * Math.floor(pixels[1]));
        sourceIndex = sourceIndex - (sourceIndex % 4);

        targetIndex = 4 * (x + w * y);
        targetIndex = targetIndex - (targetIndex % 4);

We can copy the color channels using the indices that were just computed:

        // Copy the red, green, blue and alpha channels
        targetData[targetIndex]     = sourceData[sourceIndex];
        targetData[targetIndex + 1] = sourceData[sourceIndex + 1];
        targetData[targetIndex + 2] = sourceData[sourceIndex + 2];
        targetData[targetIndex + 3] = sourceData[sourceIndex + 3];

When we finish iterating, the target image data array should be complete and ready to be drawn in the canvas container. We can clear the canvas area and copy the target image:

    // Clear the canvas element and copy the target image
    context.clearRect(0, 0, image.width, image.height);
    context.putImageData(target, 0, 0);

We obtain the Blue Marble image that is displayed using the Orthographic projection. The Blue Marble image rendered using the Orthographic projection is shown in the following screenshot:

Reprojecting the image using the Orthographic projection

There is more to know about reprojecting raster images. For instance, Jason Davies has a demo on projecting raster tiles and adding zoom behavior at http://www.jasondavies.com/maps/raster/. Also, Nathan Vander Wilt has a well-documented demo on how to use WebGL to reproject raster images using the GPU at http://andyet.iriscouch.com/world/_design/webgl/demo2.html.

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

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