Search code examples
javascriptd3.jscanvasprojectioninverse

D3 v4 invert function


I am trying to project a JPG basemap onto an Orthographic projection using the inverse projection. I have been able to get it working in v3 of D3, but I am having an issue in v4 of D3. For some reason, v4 gives me the edge of the source image as the background (rather than the black background I have specified). Are there any known issues with the inverse projection in v4 or any fixes for this?

  • D3 v4 JSBin Link

    <title>Final Project</title>
    <style>
    
    canvas {
      background-color: black;
    } 
    
    </style>
    <body>
      <div id="canvas-image-orthographic"></div>
      <script src="//d3js.org/d3.v4.min.js"></script>
    <script>
    
    // Canvas element width and height
    var width = 960,
        height = 500;
    
    // Append the canvas element to the container div
    var div = d3.select('#canvas-image-orthographic'),
        canvas = div.append('canvas')
            .attr('width', width)
            .attr('height', height);
    
    // Get the 2D context of the canvas instance
    var context = canvas.node().getContext('2d');
    
    // Create and configure the Equirectangular projection
    var equirectangular = d3.geoEquirectangular()
        .scale(width / (2 * Math.PI))
        .translate([width / 2, height / 2]);
    
    // Create and configure the Orthographic projection
    var orthographic = d3.geoOrthographic()
       .scale(Math.sqrt(2) * height / Math.PI)
       .translate([width / 2, height / 2])
       .clipAngle(90);
    
    // Create the image element
    var image = new Image(width, height);
    image.crossOrigin = "Anonymous";
    image.onload = onLoad;
    image.src = 'https://tatornator12.github.io/classes/final-project/32908689360_24792ca036_k.jpg';
    
    // 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;
    
        // 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]);
    
                // 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);
    
                    // 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);
    
                    // 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];
    
            }
    
        }
    }
    
      // Clear the canvas element and copy the target image
        context.clearRect(0, 0, image.width, image.height);
        context.putImageData(target, 0, 0);
    
    }
    
    
    </script>
    

Solution

  • The problem is that the invert function is not one to one. There are two ways that I'm aware of that can solve the problem. One, calculate the area of the disc that makes up the projection and skip pixels that are outside of that radius. Or two (which I use below), calculate the forward projection of your coordinates and see if they match the x,y coordinates that you started with:

    if ( 
      (Math.abs(x - orthographic(coords)[0]) < 0.5 ) &&
      (Math.abs(y - orthographic(coords)[1]) < 0.5 ) 
     ) 
    

    Essentially this asks is [x,y] equal to projection(projection.invert([x,y])). By ensuring that this statement is equal (or near equal) then the pixel is indeed in the projection disc. This is needed as multiple svg points can represent a given lat long but projection() returns only the one you want.

    There is a tolerance factor there for rounding errors in the code block above, as long as the forward projection is within half a pixel of the original x,y coordinate it'll be drawn (which appears to work pretty well):

    enter image description here

    I've got an updated bin here (click run, I unchecked auto run).

    Naturally this is the more computationally involved process when compared to calculating the radius of the projection disc (but that method is limited to projections that project to a disc).

    This question's two answers might be able to explain further - they cover both approaches.