Csci 210 Lab: Vizualizing Terrains

(Laura Toma)

Overview

Vast amounts of terrain data is available from remote sensing technology. Most data available is in the form of aerial photographs. Photographs are a great tool for viewing, and they became now standard with tools like Google maps or Yahoo maps and others. For modeling purposes, a more useful type of data is elevation data. Terrain elevation data can be used to model and understand a wealth of natural processes in geosciences, and beyond. Elevation data can be estimated based on images (SAR interferometry), but traditionally it has been obtained by interpolating cartographic maps; more recently it is available from technologies like LIDAR.

In this lab you will develop an application to read and render terrain elevation data.

Representing terrains

Terrains are represented by sampling points on the terrain and recording the geographical coordinates {x, y} of the point and the coresponding elevation {z} of the terrain at that point. Thus a terrain is represented as a cloud of points {x, y, z} stored in a file.

The most common terrain data available comes in form of a grid: this means that the points {x, y} are sampled uniformly with a grid. Grid elevation data is refered to as a digital elevation model or DEM, in short. In a grid terrain the individual coordinates {x, y} of the points need not be specified. It is sufficient if we know the coordinates of the upper-left corner of the grid and the spacing between sample points. If these are specified, the geographical coordinates of the grid points are implicit.

A grid terrain consists of a header, and a set of points {z} that represent the elevation of the terrain sampled with a uniform grid. Here is a grid file, set1.asc, that represents a basin in the Sierra Nevada range in the south-west. Take a good look at it.

The format of a terrain grid file is standard. The first 6 lines in the file represent the header of the terrain; it stores the number of rows in the grid, the number of columns, the geographical coordinates of the lower left corner, the spacing in the grid (assumed to be the same both horizontally and vertically), and a nodata value (this is the value that is used for points where the elevation could not be measured).

ncols         391
nrows         472
xllcorner     271845
yllcorner     3875415
cellsize      30
NODATA_value  -9999
Following the header there are nrows lines, each line containing ncols values, for a total of nrows * ncols values. These values represent the elevations sampled from the terrain. Thus, a grid terrain is basically a 2D-array of elevation values. You can assume that the elevations are integers (though you can also use floats or doubles if you want). The dimensions of the grid can be found out from the header. The other information stored in the header (xllcorner, yllcorner, cellsize, NODATA_value) you will happily ignore for this lab since we won't care about geographical coordinates---- but you have to read them anyways in order to get to the point in the file where you can start reading the elevations.

The structure of your program

You will write two classes: Grid and GridGIS. Class Grid will know to handle grid terrains, that is, read a grid from file and a render a grid. Class GridGIS will be the controller that will create and render various grids. Let's talk about each in turn.

Class Grid

Class Grid will store a grid terrain and will provide useful methods that manipulate grids. Naturally, you will store the grid as a 2D-array. You will write two main methods: a constructor to load a grid from a file, and a method to render a grid on a window.

The constructor: Reading a grid from a file

Grid (String fileName)
should take a file name as parameter. It will open the file, read the header, create the data structure to hold the elevation values, and load it with the values from the file. One thing you will need to figure out is how to work with files in Java.

Files in Java

File f = new File(fileName);
Scanner sc = null;
try {
     sc = new Scanner(f);
    } catch (FileNotFoundException e) {
     System.err.println("File not found!");
     System.exit(0);
    }
   
//here is how you would read a file of integers
while (sc.hasNextInt()) {
    int nextOne = sc.nextInt();
  
}
In a nutshell, the code creates a new File, then opens the file up with the Scanner class. Opening a scanner from a file must be checked for an exception to make sure everything went ok. Once it has been opened, then the Scanner can basically act as an interator.

If you need to revisit scanners, consult Chapter 1 textbook, and also class website which has a link to a Scanner example.

Testing your grid constructor

To test that you read the write values, write a toString method that prints all info about the grid, including the values.

Here are some test grids. Some of them (europe.asc) are large, so better not open them in a browser. Your Grid class will not be able to handle large files (try it), but it should handle the rest of them just fine. While testing, I would try getting the small ones to work before I try any of the larger ones.

If you want to play with more data, you can download your own DEMs at USGS geodata site. One of the test DEMs I included, europe.asc, comes from this site under GTOPO30 project, which contains Global 30 Arc-Second Elevation Data Set global DEM with a horizontal grid spacing of 30 arc seconds (approximately 1 kilometer). These files come in a different format, so if you want to use one of them, let me know and I will be very happy to help you convert it.

Another size where you can download (more recent) data is: here. This is SRTM data collected by NASA SRTM mission in 2000 and released for free for research and educational purposes. It covers the entire world at 3 arc second resolution (approx. 90m). It comes in tiles. Download your dream tile.

Rendering a grid:

Class Grid should provide a function to render the grid in a window. To render a grid you will loop through all the points (i,j) in the grid and draw each point (i,j) at an appropriate position and with an appropriate color.

Position: You will use the position (i,j) of the point in the grid to determine the position (x, y) of the point in the window. You will use the elevation of point (i,j) to determine the color with which you draw the point. Note that we ignore true geographical coordinates, and we use (row, column) of the point--- we don't care about the true geographical location anyways, at least, not for this lab.

You will need to figure out is how to convert from grid coordinates to window coordinates. For grids of "good" sizes, you can simply set the JFrame windows size to match the grid size. It would be nice if your program can handle small grids (therefore stretching the grid to fit on the window); and large grids, therefore finding a way to compress the grid to the window size. In either case, the window should keep the same height-width ratio as the grid, so that the grid does not look distorted.

Another small thing to be careful with is the transformation from (row,column) to window (x,y) coordinates. You want the upper-right corner of the grid (row=0,col=0) to map to the upper-right corner of the window (x=0,y=0).

Colors

On a real map elevations are coded using colors: high elevations are brown, low elevations are green. You will use the elevation of the terrain to choose the color with which you draw each point.

First, some background on colors. In Java the color is specified as a triplet (r, g, b), where r, g, b are values in the range [0.0, 1.0] or {0, 1, 2, ..255} specifying the red, green and blue intensity in the color, respectively. 0.0 represents the darkest, and 1.0 the lightest. WHITE corresponds to (1.0, 1.0, 1.0) and BLACK corresponds to (0.0, 0.0, 0.0).

Remember that on a graphics object you can change the color using

Color c = new Color (r, g, b);
g.setColor(c);

In order to make the color reflect the elevation, you will have to come up with a formula to set r,g,b as function of the elevation; you will need to do this every time you call drawLine, to set the color of the line as a function of the elevation of that line. Assume that the elevation of the line is the average of the elevation of its points.

To come up with a logic for setting r, g, b, remember that r, g, b must be values in [0.0, 1.0] (or 0..255), with 0.0 corresponding to highest elevation, and 1.0 to lowest elevation.

The easiest way would be to set all of them to the same value. Equal values for r,g and b give a gray color, so it will result in a gray-scale image.

r = g = b = ...
c = new Color(r, g, b);
g.setColor(c)

Another easy solution is to split the height range between hMIN and hMAX into a number of color buckets, and assign to all heights falling in a bucket the same color. If you chose the number of buckets large (5 or more), this will look acceptable.

The best solution is to get a smooth coloring, which, instead of assigning to all heights in an interval the asme height, it interpolates smoothly between the color at the two ends.

Extensions

So far you render a terrain by rendering each point separately. In general rendering of a surface is of better quality if the surface is represented as a triangular mesh rather than a discrete set of points (the graphics engine can compute normals to the surface to determine lighting, reflection and all sorts of other stuff). An improved renderer would view the grid as a set of triangles (see below). Instead of drawing points, draw lines between neighboring points so that overall the lines form a triangulation as below. To decide the color of a line, consider its height to be the average height of its endpoints.

Class GridGIS

The interface of GridGIS is open-ended. I imagine that class Grid will extend JFrame, and GridGIS will not, though it need not be this way. Your GridGIS class should be able to create and display DEMs. The simplest way is to create a grid from a given file in the main method of GridGIS:
Grid set1_grid = new Grid("set1.asc");
set1_grid.render();

The part that I am most interested in is the looks of your rendering. Play with colors, and make it look like a real map. Here are some good ones.
[Brunswick DEM] [set1 DEM]

As usual, encapsulate, use small functions, write nice code, write code incrementally and debug it before you move to the next step.

Have fun!