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:
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:
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.
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:
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]);
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:
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:
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:
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.