I'm always drawn to pieces of interactive data visualization when I come across them in the wild. After spending a few weeks with three.js tutorials, I started thinking about Google's Elevation API and wondering if I could plot that data in a three dimensional space. After a lot of trial, error and combing through documentation, I came up with this.

The concept is basically...

  • Draw 150 rows of 150 particles, spaced apart at regular intervals. Think of this as a grid.
  • Pick a square area of the planet with a lot of topographic diversity and draw a similar 150 x 150 grid inside of it.
  • Use the elevation API to return each point on that grid. We now have essentially an x,y,z co-ordinate of every intersection on the grid.
  • For simplicity sake, convert that latitude, altitude, and longitude to a number that makes sense on the grid. For example, the item in the center left of the grid will have an x and z value of -75, 0 (it will be 75 rows to the left of center). The y value is proportional to whatever the grid is. If each point is 0.4 miles apart, then the y value is the actual altitude divided by 0.4 miles.
  • Move each particle up or down depending on the altitude of its corresponding map point.

After the first round of initial testing, I realized there's two issues with calling the Google API in real time. One: It takes a few minutes to retrieve and process all the data and two: The API allows 1.2 million (free) requests per day, and each location uses 22,500 requests, meaning we could only hit it about 55 times in 24 hour window. After a litte bit of work in php, I came up a script that allowed me to feed a center point and a distance between points, then have that call the API and generate a json file with all the data I needed for a specific location.

A truncated version of that json file is below.

  {
    "coords":
        [
            [-75,57.8403466797,-75],
            [-74,57.8282617187,-75],
            [-73,57.8108447266,-75],
            [-72,57.7908447266,-75],
            [-71,57.7708447266,-75]
        ],
    "boundaries":[52.2238528,-117.2008759,52.1569992,-117.3097856],
    "scale":"50",
    "lowest_point":38.4
}

An unexpected challenge was determing what specific coordinates to grab. The idea was that we'd identify a specific coordinate and scale, like the peak of Mount Everest and 100 meters and have it return coordinates of every 100 meters north, east, south and west until it had 1502 coordinates. The issue with this is that the further away from the equator something is, the closer the lines of longitude are so it wasn't simply a matter each longitude co-ordinate being something like lng = originalLng * scale * i;. I was using a variation of the formula found on https://stackoverflow.com/questions/7825415/convert-nautical-miles-to-degrees-longitude-given-a-latitude-using-google-maps to figure how much to decrease or increase the longitude by given the latitude of it.

Setting the stage

When the app loads, we add the camera, renderer and controls and then add the 150 x 150 particles.

  mountainGeometry = new THREE.Geometry();
mountainGeometry.dynamic = true;
mountainGeometry.__dirtyVertices = true;
mountainGeometry.verticesNeedUpdate = true;

To start off, we need to tell three that we'll be creating new geometry and that geometry will change after the fact. Without calling mountainGeometry.__dirtyVertices, any changes we make to plot points with js won't be written to the screen.

  var pointsPlot = new Array();
var material = new THREE.PointsMaterial();
material.sizeAttenuation = false;
centerX = 0;
centerZ = 0;

totalX = 150;
totalZ = 150;

particleDistance = 25;
for ( var x = 0; x < totalX; x ++ ) {
 xplot = x - Math.round((totalX-1)/2);
 zArray = new Array();
 for ( var z = 0; z < totalZ; z ++ ) {
    var vertex = new THREE.Vector3();
    vertex.x = x*particleDistance - particleDistance*(totalX-1)/2;
    vertex.z = z*particleDistance - particleDistance*(totalZ-1)/2;
    zplot = z - Math.round((totalZ-1)/2);
    zArray[zplot] = vertex;
    mountainGeometry.vertices.push( vertex );
 }
 pointsPlot[xplot] = zArray;
}
mountainParticles = new THREE.Points( mountainGeometry, material );
mountainParticles.sortParticles = true;

For the most part, we're using default three.js functionality three.js documentation on creating particle systems but with a little twist. Because there's no way of targeting a particle by its x and z position (that I know of), we're also pushing them to a multidimensional array named pointsPlot, which is organized as
pointsPlot[xPosition][zPosition] = verticesPoint, which we'll use later to move each particle up or down. After all this, what we have is more or less, 1502 particles spaced evenly on a plane as seen below.

With totalX, totalZ we're defining the rows and columns and then multiplying them by particleDistance, which is simply a unit of scale, to space everything accordingly. Because we want the particles to be centered on the screen, the x and z positions are offset by half of the totalX and totalZ (so, each particle would really be x*particleDistance - particleDistance(totalX-1)/2 *)

Adding the altitude

Once we have the stage set, it's time to load the altitude values for each particle. This starts with an ajax call to the json file mentioned earlier and ends with the code below.

  var data = JSON.parse(request.responseText);
for(var i = 0; i<data.coords.length; i++){

  // identify which particle (x and z) we want to tween to and the y-value we want to reach
  m = data.coords[i];
  x = m[0];
  y = m[1];
  z = m[2];
  v = pointsPlot[x][z];

  // set the property we want to tween to, the value of it, and the easing 
  target = { 
     y:(y - data.lowest_point)*particleDistance, 
     ease:Power3.easeOut
  }

  // create the tween
  tween = TweenMax.to(v, 1.5, target );
  target.onUpdate = function () {
    mountainGeometry.verticesNeedUpdate = true;
  };  

}

At this point, we're looping through each co-ordinate returned and, using the first and third values, are identifying the corresponding particle based on their x and y position. The second value the altitude or y value. Some of our geometry is based on locations at sea level (like Lake Como or San Francisco) and some higher up (like K2 or Everest). Because of the major difference, and the fact that displaying the true altitude would result in some of the geometry being displayed outside of the user's screen, we're subtracting the lowest point of the geometry (data.lowest_point) from the co-ordinate's true altitude. Tweening itself is handled through use of GreenSock TweenMax which resulted in some smooth and easy-to-implement transitions.

Once the user has selected a different area of the world to explore, we simply load another json file and re-run the code above.

Thanks for reading! If you've got any questions or suggestions, leave them in the comments below!


373 2 2