spatial_search.js

Copyright 2013 Allen Institute for Brain Science Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

The mouse_connectivity_target_spatial API service takes a seed coordinate in the adult mouse reference space and returns the list of experiments with a signal density threshold above 0.1 at that coordinate. Each experiment also has a list of coordinates that describe the shortest path through the fluorescent signal from the seed point to the center of the experiment’s primary injection site. This query can be used to reproduce the visualization of the spatial search results on the connectivity projection search page.

This example is a simple 3D visualization of the experiments returned by the mouse_connectivity_target_spatial service. It uses the three.js javascript library.

var spatial_search_path = "http://api.brain-map.org/api/v2/data/query.json?criteria=service::mouse_connectivity_target_spatial";

var seed_point = [ 7100, 3700, 7850 ];

var mouseX = 0, mouseY = 0;
var targetRotationX = 0, targetRotationY = 0;
var targetRotationXOnMouseDown = 0, targetRotationYOnMouseDown = 0;

var mouseXOnMouseDown = 0, mouseYOnMouseDown = 0;

var container = null;

var scale = 0.01;
var dimensions = [ 133, 81, 115 ];

var current_request = null;
var queued_request = null;

var camera, controls, scene, renderer, light, outline;

Kick things off when the page is ready.

$(function() {
  initialize();
});

Download the spatial search lines for the current seed point, then initialize the three.js visualization.

function initialize() {
  var urlvars = getUrlVars();
  if ('seed_point' in urlvars) {
    seed_point = urlvars['seed_point'].split(',').map(function(c) { return parseInt(c); });
  }
  update_spatial_search_link();

  container = document.getElementById('chart');

If the browser supports WebGL, download the data and initialize the 3D components.

  if ( Detector.webgl ) {
    download_data(function(data) { 
      initialize_spinners();
      initialize_threejs(data); 
    });
  } else {
    $(container).append(
      $('<div>').addClass('svgError')
        .html("This demo requires WebGL support. " +
          "Click <a href='http://caniuse.com/webgl'>here</a> " + 
            "to see which browsers support WebGL."));
  }
}

Three jQuery UI spinners control the search seed point (one each for x, y, and z).

function initialize_spinners() {
  $("#seed_point_x").spinner({
    change: on_x_seed_point_changed,
    stop: on_x_seed_point_changed,
    min: 0,
    max: dimensions[0] * 100,
    step: 100
  }).val(seed_point[0]);

  $("#seed_point_y").spinner({
    change: on_y_seed_point_changed,
    stop: on_y_seed_point_changed,
    min: 0,
    max: dimensions[1] * 100,
    step: 100
  }).val(seed_point[1]);

  $("#seed_point_z").spinner({
    change: on_z_seed_point_changed,
    stop: on_z_seed_point_changed,
    min: 0,
    max: dimensions[2] * 100,
    step: 100
  }).val(seed_point[2]);
}

These three methods are called when a spinner’s value changes. They trigger a new spatial search and update the displayed geometry.

function on_x_seed_point_changed(event) {
  seed_point[0] = parseInt($("#seed_point_x").val());
  update();
}

function on_y_seed_point_changed(event) {
  seed_point[1] = parseInt($("#seed_point_y").val());
  update();
}

function on_z_seed_point_changed(event) {
  seed_point[2] = parseInt($("#seed_point_z").val());
  update();
}

A simple function to update the search link as the seed point changes.

function update_spatial_search_link() {
  var spatial_search_link = "http://connectivity.brain-map.org/?searchMode=spatial&sourceDomain=8&primaryStructureOnly=true&targetCoordinates=" + seed_point.join(',');
  $("#spatial_search_link").attr("href", spatial_search_link);

}

Call when the seed point changes. Updates the search link and geometry.

function update() {
  update_spatial_search_link();
  download_data(function(data) {
    initialize_geometry(data);
    renderer.render( scene, camera );
  })
}

Perform the spatial search. Doing a lot of these can hurt browser performance, so only the most recent request is actually made.

function download_data(on_success) {
  $(".loading").show();

If there’s an API request already en route, queue the state of this request for later. The queued request will be overwritten by subsequent calls until the original request finishes.

  if (current_request) {
    queued_request = {
      queued_seed_point: [ seed_point[0], seed_point[1], seed_point[2] ],
      queued_on_success: on_success
    };
    return;
  }

Construct the URL for the spatial search query.

  var url = spatial_search_path + "[seed_point$eq" + seed_point.join(',') + "]";

  current_request = $.ajax(url, {
    success: function(response) {
      current_request = null;
      on_success(response['msg']);

      $(".loading").hide();

If there’s a queued request, trigger it now.

      if (queued_request) {
        seed_point = queued_request.queued_seed_point;
        download_data(queued_request.queued_on_success);
        queued_request = null;
      }
    },
    error: function(error) {
      $(".loading").html("Error with API query: " + url);
    }
  });
}

Initialize all of the components required by three.js once the data exists.

function initialize_threejs(experiments) {

  initialize_scene();
  initialize_outline();
  initialize_geometry(experiments);

  animate();
}

function initialize_scene() {
  var jqcontainer = $(container);
  var width = jqcontainer.width();
  var height = jqcontainer.height();

Build the camera (field of view, aspect ratio, clipping planes, orientation, position).

  camera = new THREE.PerspectiveCamera( 33, width / height, 1, 10000 );
  camera.up.x = 0;
  camera.up.y = -1;
  camera.up.z = 0;
  camera.position.z = -300;

Initialize the mouse controls to allow pan/rotate/zoom.

  controls = new THREE.TrackballControls( camera, container );
  controls.rotateSpeed = 2.0;
  controls.zoomSpeed = 1.2;
  controls.panSpeed = 0.8;
  
  controls.noZoom = false;
  controls.noPan = false;
  
  controls.staticMoving = true;
  controls.dynamicDampingFactor = 0.3;
  
  controls.keys = [ 65, 83, 68 ];
  
  controls.addEventListener( 'change', render );
  

The scene will hold all of the lines and spheres.

  scene = new THREE.Scene();

  renderer = new THREE.WebGLRenderer( { antialias: true } );
  renderer.setClearColor(0x000000);
  renderer.setSize( width, height );

Don’t forget to add the WebGL element to the DOM.

  container.appendChild( renderer.domElement );

This is a light that will track the camera position.

  light = new THREE.PointLight(0xFFFFFF);
  scene.add(light);
}

Make the wireframe outline around the reference space.

function initialize_outline() {
  var wireframe_geometry = new THREE.CubeGeometry( dimensions[0],
                           dimensions[1],
                           dimensions[2] );
  
  var wireframe_material = new THREE.MeshBasicMaterial({ color: 0xFFFFFF, 
                               shading: THREE.FlatShading, 
                               wireframe: true, 
                               wireframeLinewidth: 1 } );
  
  outline = new THREE.Mesh( wireframe_geometry, wireframe_material );

  scene.add( outline );
}

Make the sphere centered at the seed point.

function initialize_seed_sphere(coord) {
  var seed_geometry = new THREE.SphereGeometry( 2, 8, 8 );
  var seed_material = new THREE.MeshLambertMaterial({ color: 0xFFFFFF });
  var seed_sphere = new THREE.Mesh( seed_geometry, seed_material );
  seed_sphere.position.x = coord[0] * scale - dimensions[0] * 0.5;
  seed_sphere.position.y = coord[1] * scale - dimensions[1] * 0.5;
  seed_sphere.position.z = coord[2] * scale - dimensions[2] * 0.5;
  scene.add( seed_sphere );
}

Make the spheres centered at the injection sites of each experiment.

function initialize_spheres(experiments) {
  for (var i = 0; i < experiments.length; i++) {
    var exp = experiments[i];
    var injection_volume = exp['injection-volume'];
    var coord = exp['injection-coordinates'];

Color the line by its injection structure color.

    var colorint = parseInt(exp['structure-color'], 16);
    

The sphere should have roughly the same volume as the injection site.

    var radius = Math.pow(3 * Math.PI / 4 * injection_volume, 1/3);
    
    var sphere_geometry = new THREE.SphereGeometry( radius, 8, 8);
    var sphere_material = new THREE.MeshLambertMaterial({ color: colorint });
    var sphere = new THREE.Mesh( sphere_geometry, sphere_material );

The coordinates are in microns. World coordinates are in reference space voxel 100um coordinates. Scale the coordinate accordingly and translate to the center of the reference space cube.

    sphere.position.x = coord[0]*scale - dimensions[0] * 0.5;
    sphere.position.y = coord[1]*scale - dimensions[1] * 0.5;
    sphere.position.z = coord[2]*scale - dimensions[2] * 0.5;

    scene.add( sphere );
  }
}

Make the path lines that connect an experiment’s injection site to the seed point.

function initialize_lines(experiments) {
  for (var i = 0; i < experiments.length; i++) {
    var exp = experiments[i];
    var path = exp.path;

    if (!path || path.length == 0)
      continue;

Create a line strip for all of the path coordinates.

    var geometry = new THREE.Geometry();
    for (var j = 0; j < path.length; j++) {
      geometry.vertices.push( new THREE.Vector3( path[j].coord[0], path[j].coord[1], path[j].coord[2] ) );
    }

Color the line by its injection structure color.

    var colorint = parseInt(exp['structure-color'], 16);
    material = new THREE.LineBasicMaterial( { color: colorint, opacity: 1, linewidth: 1 } );
    

The coordinates are in microns, so set a global vertex position scale and then translate to the center of the reference space.

    var line = new THREE.Line(geometry, material);
    line.scale.x = line.scale.y = line.scale.z =  scale;
    line.position.x = -dimensions[0] * 0.5;
    line.position.y = -dimensions[1] * 0.5;
    line.position.z = -dimensions[2] * 0.5;

    scene.add( line );
  }
}

Clear the scene and reinitialize all of the geometry that can change.

function initialize_geometry(experiments) {
  for ( var i = scene.children.length - 1; i >= 0 ; i -- ) {
    obj = scene.children[ i ];
    if (obj !== camera && obj != light && obj != outline) {
      scene.remove(obj);
    }
  }

  initialize_seed_sphere(seed_point);
  initialize_spheres(experiments);
  initialize_lines(experiments);
}

This function uses requestAnimationFrame, which attempts to render at a consistent 60 fps (at most).

function animate() {
  requestAnimationFrame( animate );
  controls.update();
}

function render() {

No need to render if the camera position hasn’t change.

  if (light.position.x != camera.position.x ||
    light.position.y != camera.position.y ||
    light.position.z != camera.position.z) {

Move the light to the camera’s position.

    light.position.x = camera.position.x;
    light.position.y = camera.position.y;
    light.position.z = camera.position.z;
    
    renderer.render( scene, camera );
  }
}

Read a page’s GET URL variables and return them as an associative array.

function getUrlVars()
{
    var vars = [], hash;
    var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
    for(var i = 0; i < hashes.length; i++)
    {
        hash = hashes[i].split('=');
        vars.push(hash[0]);
        vars[hash[0]] = hash[1];
    }
    return vars;
}