Musings of a Fondue

Project MViz - Update 3

How to do picking (i.e. country selection on mouseclick)?

I came across this project by Steve Hall. In it he makes a 3d map using Three.js and D3 that is responsive to mouseover and click events. He also has a great writeup that shares how he tackled the problem.

At the time, I didn’t really understand what the article was saying. But two things I did takeaway were,

  1. the use of Three.js’s built-in raycasting to figure out the mouse’s coordinates in 3d space
  2. the use of an algorithm to determine if a point is inside a bounded region

2D Picking

A bit of searching later, I came across the related Wikipedia article (Point in Polygon). Even luckier, I came across this Stack Overflow answer which mentions that HTML Canvas already has a built-in, optimized algorithm (isPointInPath()) for doing this detection. Yay!

With this in mind, I reworked my map drawing code a bit. Specifically, I saved each continent’s path as a Path2D object. And on mousemove, simply called isPointInPath( continentPath ).

Here is a demo. (Mouseover the continents to see their names.)

And here’s the code,


// Initialize
for( var j = 0; j < rawishCoords.length; j++ ){

    // Use Path2D() to store path data for later access, stackoverflow.com/a/28913470
    var country = new Path2D( rawishCoords[j].d );

    // draw the path
    ctx.strokeStyle = "blue";
    ctx.stroke(country);

    rawishCoords[j].canvasPath = country;
}

// Listen for mousemove
canvas.addEventListener('mousemove', function(evt) {

    var mousePos = getMousePos(c, evt);
    whereIsThisPoint( mousePos.x, mousePos.y );

}, false);

// Check if point in path
var whereIsThisPoint = function( px, py ){
    // Use canvas's built-in isPointInPath() method
    //  developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath

    for( var i = 0; i < rawishCoords.length; i++ ){
        
        var pointInPath = ctx.isPointInPath( rawishCoords[i].canvasPath, px, py);

        if ( pointInPath ){
            console.log( rawishCoords[i].id );
            document.getElementById("textDiv").textContent = rawishCoords[i].id;
        }       
    }
};

3D Picking

As for the 3D raycasting, I used one of Lee Stemkoski’s excellent Three.js demos, in combination with the official docs to get it working.

The code for this is as shown in the docs,


var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();

function onMouseMove( event ) {

    // calculate mouse position in normalized device coordinates
    // (-1 to +1) for both components

    mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
    mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; 
}

function render() {

    // update the picking ray with the camera and mouse position    
    raycaster.setFromCamera( mouse, camera );   

    // calculate objects intersecting the picking ray
    var intersects = raycaster.intersectObjects( scene.children );

    for ( var i = 0; i < intersects.length; i++ ) {

        intersects[ i ].object.material.color.set( 0xff0000 );  
    }
    
    renderer.render( scene, camera );
}

window.addEventListener( 'mousemove', onMouseMove, false );

window.requestAnimationFrame(render);

Play with it here. (Mouseover makes a face turn red.)

Combining the two

Coming soon.

The idea, would be to convert the 3D mouse coordinates to 2D coordinates, and use those with the isPointInPath() method.

Comments