Thursday, 26 March 2009

Drawing grids in 2D

The code for a 2D drawGrid() can be something like this:

void drawGrid(int left, int top, int width, int height, int xCells,
int yCells, boolean drawOutline){

// precondition for drawing the grid
if (width <= 0 || height <= 0) return;

// compute the coordinates of the rightmost an bottom pixels
int right = left + width - 1;
int bottom = top + height - 1;

// draw the grid
drawVertGrid(left, top, right, bottom, width, xCells);
drawHorzGrid(left, top, right, bottom, height, yCells);

if (drawOutline){
// save current line width
float curWeight = g.strokeWeight;
// draw outline with a line 1 pixel wide
line(left, top, right, top);
line(right, top, right, bottom);
line(right, bottom, left, bottom);
line(left, bottom, left, top);
// restore line width
Listing CC.3

Preconditions for execution

Writing functions it is wise to check that all possible values passed as arguments can be dealt with by the code in the function body. What happens, for example, if drawGrid() is called with zero or negative dimensions passed to width and/or height?

If we see there is a problem a number of options are available. In this case a fully justifiable option is to simply ignore it. We are not trying to create a flawless machine and an emergent property of a program may turn out to be a spectacular work of art. On the other hand, if the code simply does not work a lot of time can be wasted trying to track down the bug.

Another option is to pass the problem on to other functions. Perhaps the grid will be drawn with all or part of it off the screen. This eventuality can be passed on for line() to deal with.

The third option is to write code at the start of the function that checks input is within range. These checks are known as preconditions: execution of other code in the function body is conditional on the checks being passed. There are three ways of dealing with a failed precondition: the function can return immediately to the caller; likewise but it returns a Boolean or some other value indicating success or failure; alternatively an error condition can be created. Java error conditions are dealt with by throwing and catching exception objects, certainly beyond the scope of this Chapter. For drawing grids a simple conditional return statement is appropriate:
      if (width <= 0 || height <= 0) return;

Note that other problems such as zero or negative numbers of cells are passed on to the grid-line drawing functions.

Minimizing coding

Another useful strategy is to avoid writing code that does the same thing more than once. This makes the code run faster but more importantly can make it shorter and easier to read. We are going to draw numerous lines from the left to the right of the grid and from its top to the bottom so compute right and bottom before doing anything else:
      int right = left + width - 1;
int bottom = top + height - 1;

The next task is to draw the gridlines, done by two functions:
      drawVertGrid(left, top, width, bottom, xCells, constrain);
drawHorzGrid(left, top, right, height, yCells, constrain);
Finally a one pixel wide outline is drawn around the grid if this option is set having first saved the current line thickness, as shown in Listing CC.3. Drawing the outline, and in calls to the drawing functions, the values of right and bottom computed on entry save numerous calculations.

So, you may be thinking, if working out this stuff early is such a good thing why not do it in the doDrawing() function, pass these values to drawGrid()and maybe make some savings in other functions called from doDrawing()? There are a few reasons for not doing this but the most important relates to abstraction. Working in doDrawing() we should not have to consider the needs of drawGrid() or any other functions called, only what they can do to get the drawing done in terms of the size, shape and location of the objects drawn.

Drawing uniform cells

The tasks to be done by the grid-line drawing functions are: check preconditions, compute cell size, initialise the drawing position to the first grid line and finally, loop while the drawing position is less than the last pixel, drawing each grid line and incrementing position by cell size. Coding these tasks to draw vertical grid lines gets:

void drawVertGrid(int left, int top, int right, int bottom, int width,
int xCells){

// precondition: if negative or zero cells exit now
if (xCells <= 0) return;

// compute cell width
float cellWidth = (float)width / xCells;

if (cellWidth < 1)
cellWidth = 1:

for (float x = left + cellWidth; x < right; x += cellWidth){

// x is the x-coordinate of the vertical line drawn in the window
line(x, top, x, bottom);

There are two points to watch out for computing cellWidth. The first one is division by zero. Java and Processing allow division by zero for floating point calculations without throwing a fit but an INFINITY result is usually best avoided (do it with integers and a divide by zero exception is thrown). In this case a zero divisor has already been excluded by the precondition: xCells must be greater than zero. Negative and zero values for width were excluded by drawGrid() making certain that cellWidth is positive. If cellWidth was zero or negative adding it to x in the loop would never make x equal to or greater than right and the loop would not exit.

Writing mission critical software - perhaps designed to avoid crashing our lauch vehicle in a nearby swamp or bringing a spacecraft to a perfect landing two miles below the surface - it would be wise to do similar checks in both the caller and the called function. As things stand a single check will do nicely. Excluding a negative value for xCells in drawVertGrid() rather than drawGrid() includes the option that other versions of drawVertGrid() will process such values to get a particular visual effect.

The second point is that Java’s multipurpose division operator returns an integer result when both operands are integers. The drawGrid() specification requires that cells fit the grid. To make this work, cell dimensions need to be floating point values so that when added together fractions of a pixel periodically add up to a whole one. The integer argument width is typecast to(float) getting a floating point result complete with any fractional part. Lastly, if cellWidth is purely fractional, the entire grid will consist of minutely overlapping lines and may take a while to draw. In this case cell width is set to unity.

The drawing loop

Using a for loop keeps lines of code to a minimum. You will recollect that the format of a for loop is generally:

for (initialize loop variable;
condition for entering or re-entering the loop;
action to be done at the end of each pass through the loop){

// stuff to be done in the loop goes here

} // end of the loop

At the start of the loop a line at the left of the grid is not required so the loop variable x is declared and initialised to draw the first line at left + cellWidth . Declaring the loop variable in the loop declaration makes it accessible only within the loop, a safety advantage over a while loop in many cases because its value after exit can be somewhat obscure.

Java for loops are not the simple counting loops seen in some other languages. Basically a for loop provides a concise way of writing a while loop by filling in the items inside the round brackets. Ideally the condition for entry or re-entry should specify a range of loop variable values that allows access to the loop - not a single value that when reached by incrementing or decrementing the variable terminates looping: loop while x != right; x += cellWidth, for example. Using an inequality exit condition there is a danger that x will be initialized greater than right permitting unplanned entry or that, due to a fractional initial value and/or an increment greater than or smaller than unity, x steps over an assumed exit value. Result: the loop loops-for-ever. The condition x < right avoids these problems.

The last part of the for loop declaration is executed after each pass through the loop. Here it adds cellWidth to the loop variable using the shorthand += operator to take it to the coordinate of the next grid line (approximately). In this case 'stuff to be done in the loop' is simply to draw the current line going top to bottom. The function that draws horizontal grid lines is similar:

void drawHorzGrid(int left, int top, int right, int bottom, int height,
int yCells){

// precondition: if negative or zero cells exit now
if (yCells <= 0) return;

// compute cell height
float cellHeight = (float)height / yCells;
if (cellHeight < 1)
cellHeight = 1;

for (float y = top + cellHeight; y < bottom; y += cellHeight){

// y is the y-coordinate of the horizontal line drawn in the window
line(left, y, right, y);

No comments:

Post a Comment