Contouring Data Fields

Scientific data is often in gridded data format, and contouring is a way to view it.


June 01, 1992
URL:http://www.drdobbs.com/database/contouring-data-fields/184408780

Figure 1


Copyright © 1992, Dr. Dobb's Journal

Figure 2


Copyright © 1992, Dr. Dobb's Journal

Figure 3


Copyright © 1992, Dr. Dobb's Journal

Figure 4


Copyright © 1992, Dr. Dobb's Journal

JUN92: CONTOURING DATA FIELDS

CONTOURING DATA FIELDS

Maps are simply an application of gridded data formats

Bruce (Bear) Giles

Bear, a programmer for National Systems & Research, can be reached at 325 Broadway, MC 425, R/E/FS4, Boulder, CO 80303 or [email protected].


A great deal of scientific and engineering data is available in a gridded (or regularly sampled) data format. For example, the Nested Grid Model (NGM) used by the National Weather Service consists of a series of atmospheric measurements at every 1.25 degrees of latitude and 2.5 degrees of longitude across the continental United States. This data is generated by supercomputers in Suitland, Maryland every 12 hours.

The most common way to view such data is through contouring. An example of contoured data familiar to many people is the topological maps used by hikers and skiers. Such maps are much easier to use than books specifying the elevation of thousands of different locations.

Properties of Contour Lines

Contour lines of gridded data have several properties: They never cross, never split or join other contour lines, and never stop, except at the edge of the data field.

While it is easy to imagine situations where the physical contour lines violate one or more of these rules, such situations never arise in sampled data. The closest possibility is the unlikely event of adjoining data points having precisely the same value as the contour level. This could split the contour line, but because this causes a division-by-zero error, in these cases the contour line is simply stopped. In practice this rarely occurs.

We are using a linear interpolation between data points, so no more than one contour line ever crosses any grid cell-face for any contour level. (This is not guaranteed if more powerful interpolation techniques are used.)

Allowing for rotations, it is easy to show that there are only four possible orientations of a contour line within a single grid cell; see Figure 1. The final possibility has a symmetrical alternative, as in Figure 2. Either of these last two possibilities may be used as long as a consistent choice is made for each contour level.

As mentioned, a simple linear interpolation provides the precise entry and exit points of the grid cell. A straight line is then drawn between these points to form the contour. While it may be tempting to apply curve-smoothing functions (such as cubic splines) to the contour lines, this should be avoided. If curve smoothing is done, there is an excellent chance of erroneous results (such as crossed contour lines).

The Algorithm

The contouring algorithm is quite simple: Ignoring boundary conditions for the moment, we simply find any cell with a contour line within it and successively apply the preceding rules. The contour line can only cross a cell-face once, so maintaining a simple bitmap of all crossed cell-faces can prevent endless processing of closed contours.

Of course, in the real world contours often cross the edge of the data field. To handle this, each border is scanned for inward contour lines, and these contours are followed until another border is encountered. The aforementioned bitmap of cell-faces is not cleared as these contour lines are generated. All interior cell-faces are then scanned for unmarked contour lines. These form closed contour lines and are followed until the contour returns to its initial point.

After all interior points have been checked, the bitmap is cleared and the next level (if any) is contoured.

The Contouring Code

Listing One (page 91) shows the contouring routine. Listing Two (page 95) contains a simple PostScript file generator illustrating the requirements of Polyline() and Text(). Finally, Listing Three (page 95) contains a sample application that generates a data field and then contours it.

The contour routine is called with four arguments: a pointer to the data field itself, its width and height, and the contouring interval. In the accompanying code floats are used to save space, although when contouring a nearly flat field it may be necessary to promote all floats to doubles for accuracy.

The data field is assumed to be stored in row-major order, starting at the upper-left corner. All data points must contain valid data, although the extensions to allow for missing data are straightforward.

The application writer must provide two extra routines, called by the contouring module. The first is Polyline(), which is called with a list of points on the contour line. All coordinates are within the range 0 to 1.

The second routine is Text(), which is used to label the contour lines. It is called with a text string and two coordinates, once again within the range 0 to 1.

Extensions

As mentioned before, modifying the code to allow for missing data and/or irregular edges is relatively straightforward. The primary headache is modifying startEdge() to follow a jagged border.

In many cases, nonrectangular grids are trivial to support. As long as each contour cell has four corners, the precise topology of the data points is irrelevant; simply remap the coordinates within Polyline() and Text(). For triangular cells the algorithm is even simpler than for rectangular cells, as there are fewer cases or rotations to deal with.

Filled contours, similar to the weather map in USA Today, are straightforward in concept. Perform a polygon fill (as described in Michael Abrash's February 1991 "Graphics Programming" column) on the polygon formed by the current contour line connected to all interior contour lines. (For example, contour lines A, B, and C would form the larger polygon a..ab..bac..ca where x..x is all points within contour line X.) Unfortunately, it requires substantial analysis to determine which contour lines lie "within" another contour line; see Figure 3 and Figure 4.

Determining the area within a contour line can be accomplished by appropriately modifying the preceding modification. Simply count the number of pixels which would have been set had the fill been performed, and divide by the total number of pixels within the display.


_CONTOURING DATA FIELDS_
by Bruce (Bear) Giles


[LISTING ONE]


#include    <stdio.h>
#include    <math.h>
#include    <malloc.h>
#include    <values.h>

#if defined (NEVER)
#include    <ieeefp.h>
#else
#define     NaN         0xFFFFFFFF
#define     isnanf(x)   ((x) == NaN)
#endif

typedef unsigned    short   ushort;
typedef unsigned    char    uchar;

#define DEFAULT_LEVELS  16

/* Mnemonics for contour line bearings  */
#define EAST        0
#define NORTH       1
#define WEST        2
#define SOUTH       3

/* Mnemonics for relative data point positions  */
#define SAME        0
#define NEXT        1
#define OPPOSITE    2
#define ADJACENT    3

/* Bit-mapped information in 'map' field.  */
#define EW_MAP      0x01
#define NS_MAP      0x02

typedef struct
    {
    float   x, y;
    }   LIST;
typedef struct
    {
    short   dim_x;          /*  dimensions of grid array... */
    short   dim_y;
    float   max_value;      /*  statistics on the data...   */
    float   min_value;
    float   mean;
    float   std;
    short   contour_mode;   /*  control variable            */
    float   first_level;    /*  first (and subsequent)      */
    float   step;           /*    contour level             */
    char    format[20];     /*  format of contour labels    */
    float   *data;          /*  pointer to grid data        */
    char    *map;           /*  pointer to "in-use" map     */
    LIST    *list;          /*  used by 'Polyline()'        */
    ushort  count;
    }   GRID;
typedef struct
    {
    short   x;
    short   y;
    uchar   bearing;
    }   POINT;
#define MXY_to_L(g,x,y)     ((ushort) (y) * (g)->dim_x + (ushort) (x) + 1)
#define XY_to_L(g,x,y)      ((ushort) (y) * (g)->dim_x + (ushort) (x))

extern  void    Text();
extern  void    Polyline();

/* Contour generation. */
void    Contour();

int     scaleData();
void    startLine();

void    startInterior();
void    drawLine();

void    markInUse();
uchar   faceInUse();
float   getDataPoint();

void    initPoint();
void    lastPoint();
uchar   savePoint();

/* inc     > 0 increment to use between contour levels.
 *         < 0 number of contour levels to generate [abs(inc)].
 *         = 0 generate default number of contour levels.
 */
void    Contour (data, dim_x, dim_y, inc)
float   *data;
int     dim_x;
int     dim_y;
double  inc;
    {
    GRID    grid;

    grid.data   = data;
    grid.dim_x  = dim_x;
    grid.dim_y  = dim_y;

    /* Allocate buffers used to contain contour information */
    if ((grid.map = malloc ((dim_x + 1) * dim_y)) == NULL)
        {
        fprintf (stderr, "Contour(): unable to allocate buffer! (%d bytes)\n",
            (dim_x + 1) * dim_y * sizeof (LIST));

        free ((char *) grid.map);
        return;
        }
    grid.list = (LIST *) malloc (2 * dim_x * dim_y * sizeof (LIST));
    if (grid.list == (LIST  *) NULL)
        {
        fprintf (stderr, "Contour(): unable to allocate buffer! (%d bytes)\n",
            2 * dim_x * dim_y * sizeof (LIST));

        free ((char *) grid.map);
        return;
        }
    /* Generate contours, if not a uniform field. */
    if (scaleData (&grid, inc))

        startLine (&grid);
    /* Release data structures. */
    free ((char *) grid.map);
    free ((char *) grid.list);
    }

/* scaleData--Determine necessary statistics for contouring data set: global
 * maximum & minimum, etc. Then initialize items used by rest of module. */
int     scaleData (grid, inc)
GRID    *grid;
double  inc;
    {
    ushort  i;
    float   step, level;
    float   sum, sum2, count;
    float   p, *u, *v, r;
    char    *s;
    short   n1, n2;
    int     first, n;
    long    x;

    sum = sum2 = count = 0.0;

    first = 1;
    s = grid->map;
    u = grid->data;
    v = u + grid->dim_x * grid->dim_y;
    for (i = 0; i < grid->dim_x * grid->dim_y; i++, u++, v++, s++)
        {
        r = *u;
        sum   += r;
        sum2  += r * r;
        count += 1.0;

        if (first)
            {
            grid->max_value = grid->min_value = r;
            first = 0;
            }
        else if (grid->max_value < r)
            grid->max_value = r;
        else if (grid->min_value > r)
            grid->min_value = r;
        }
    grid->mean = sum / count;
    if (grid->min_value == grid->max_value)
        return 0;
    grid->std  = sqrt ((sum2 - sum * sum /count) / (count - 1.0));
    if (inc > 0.0)
        {
        /* Use specified increment */
        step = inc;
        n = (int) (grid->max_value - grid->min_value) / step + 1;

        while (n > 40)
            {
            step *= 2.0;
            n = (int) (grid->max_value - grid->min_value) / step + 1;
            }
        }
    else
        {
        /* Choose [specified|reasonable] number of levels and normalize
         * increment to a reasonable value. */
        n = (inc == 0.0) ? DEFAULT_LEVELS : (short) fabs (inc);

        step = 4.0 * grid->std / (float) n;
        p    = pow (10.0, floor (log10 ((double) step)));
        step = p * floor ((step + p / 2.0) / p);
        }
    n1 = (int) floor (log10 (fabs (grid->max_value)));
    n2 = -((int) floor (log10 (step)));

    if (n2 > 0)
        sprintf (grid->format, "%%%d.%df", n1 + n2 + 2, n2);
    else
        sprintf (grid->format, "%%%d.0f", n1 + 1);
    if (grid->max_value * grid->min_value < 0.0)
        level = step * floor (grid->mean / step);           /*  odd */
    else
        level = step * floor (grid->min_value / step);
    level -= step * floor ((float) (n - 1)/ 2);

    /* Back up to include add'l levels, if necessary */
    while (level - step > grid->min_value)
        level -= step;

    grid->first_level = level;
    grid->step = step;
    return  1;
    }
/* startLine -- Locate first point of contour lines by checking edges of
   gridded data set, then interior points, for each contour level. */
static
void    startLine (grid)
GRID    *grid;
    {
    ushort  idx, i, edge;
    double  level;
    for (idx = 0, level = grid->first_level; level < grid->max_value;
        level += grid->step, idx++)
        {

        /* Clear flags */
        grid->contour_mode = (level >= grid->mean);
        memset (grid->map, 0, grid->dim_x * grid->dim_y);

        /* Check edges */
        for (edge = 0; edge < 4; edge++)
            startEdge (grid, level, edge);
        /* Check interior points */
        startInterior (grid, level);
        }
    }
/* startEdge -- For a specified contour level and edge of gridded data set,
 *  check for (properly directed) contour line. */
static
void    startEdge (grid, level, bearing)
GRID    *grid;
float   level;

uchar   bearing;
    {
    POINT   point1, point2;
    float   last, next;
    short   i, ds;

    switch (point1.bearing = bearing)
        {
        case EAST:
            point1.x = 0;
            point1.y = 0;
            ds = 1;
            break;
        case NORTH:
            point1.x = 0;
            point1.y = grid->dim_y - 2;
            ds = 1;
            break;
        case WEST:

            point1.x = grid->dim_x - 2;
            point1.y = grid->dim_y - 2;
            ds = -1;
            break;
        case SOUTH:
            point1.x = grid->dim_x - 2;
            point1.y = 0;
            ds = -1;
            break;
        }
    switch (point1.bearing)
        {
            /* Find first point with valid data. */
        case EAST:
        case WEST:
            next = getDataPoint (grid, &point1, SAME);
            memcpy ((char *) &point2, (char *) &point1, sizeof (POINT));
            point2.x -= ds;

            for (i = 1; i < grid->dim_y; i++, point1.y = point2.y += ds)
                {
                last = next;
                next = getDataPoint (grid, &point1, NEXT);
                if (last >= level && level > next)
                   {
                   drawLine (grid, &point1, level);
                   memcpy ((char *) &point1, (char *) &point2, sizeof (POINT));
                   point1.x = point2.x + ds;

                   }
                }
            break;
            /* Find first point with valid data. */
        case SOUTH:
        case NORTH:
            next = getDataPoint (grid, &point1, SAME);
            memcpy ((char *) &point2, (char *) &point1, sizeof (POINT));
            point2.y += ds;

            for (i = 1; i < grid->dim_x; i++, point1.x = point2.x += ds)
                {
                last = next;
                next = getDataPoint (grid, &point1, NEXT);
                if (last >= level && level > next)
                    {
                   drawLine (grid, &point1, level);

                   memcpy ((char *) &point1, (char *) &point2, sizeof (POINT));
                   point1.y = point2.y - ds;
                   }
                }
            break;
        }
    }
/* startInterior -- For a specified contour level, check for (properly
directed) contour line for all interior data points. Do _not_ follow contour
lines detected by the 'startEdge' routine. */
static
void    startInterior (grid, level)
GRID    *grid;
float   level;
    {
    POINT   point;
    ushort  x, y;
    float   next, last;
    for (x = 1; x < grid->dim_x - 1; x++)
        {
        point.x = x;
        point.y = 0;
        point.bearing = EAST;
        next = getDataPoint (grid, &point, SAME);
        for (y = point.y; y < grid->dim_y; y++, point.y++)

            {
            last = next;
            next = getDataPoint (grid, &point, NEXT);
            if (last >= level && level > next)
                {
                if (!faceInUse (grid, &point, WEST))
                    {
                    drawLine (grid, &point, level);
                    point.x = x;
                    point.y = y;
                    point.bearing = EAST;
                    }
                }
            }
        }
    }
/* drawLine -- Given in initial contour point by either 'startEdge' or
'startInterior', follow contour line until it encounters either an edge
or previously contoured cell. */
static
void    drawLine (grid, point, level)
GRID    *grid;
POINT   *point;
float   level;
    {
    uchar   exit_bearing;
    uchar   adj, opp;
    float   fadj, fopp;

    initPoint (grid);

    for ( ;; )
        {
        /* Add current point to vector list. If either of the points is
         * missing, return immediately (open contour). */
        if (!savePoint (grid, point, level))
            {
            lastPoint (grid);
            return;
            }
        /* Has this face of this cell been marked in use? If so, then this is
         * a closed contour. */
        if (faceInUse (grid, point, WEST))
            {
            lastPoint (grid);
            return;
            }
        /* Examine adjacent and opposite corners of cell; determine
         * appropriate action. */
        markInUse (grid, point, WEST);

        fadj = getDataPoint (grid, point, ADJACENT);
        fopp = getDataPoint (grid, point, OPPOSITE);

        /* If either point is missing, return immediately (open contour). */
        if (isnanf (fadj) || isnanf (fopp))
            {
            lastPoint (grid);
            return;
            }
        adj = (fadj <= level) ? 2 : 0;
        opp = (fopp >= level) ? 1 : 0;
        switch (adj + opp)
            {
            /* Exit EAST face. */
            case 0:
                markInUse (grid, point, NORTH);
                markInUse (grid, point, SOUTH);
                exit_bearing = EAST;
                break;
            /* Exit SOUTH face. */
            case 1:
                markInUse (grid, point, NORTH);
                markInUse (grid, point, EAST);
                exit_bearing = SOUTH;
                break;
            /* Exit NORTH face. */
            case 2:
                markInUse (grid, point, EAST);

                markInUse (grid, point, SOUTH);
                exit_bearing = NORTH;
                break;
            /* Exit NORTH or SOUTH face, depending upon contour level. */
            case 3:
                exit_bearing = (grid->contour_mode) ? NORTH : SOUTH;
                break;
            }
        /* Update face number, coordinate of defining corner. */
        point->bearing = (point->bearing + exit_bearing) % 4;
        switch (point->bearing)
            {
            case EAST : point->x++; break;
            case NORTH: point->y--; break;
            case WEST : point->x--; break;
            case SOUTH: point->y++; break;
            }
        }
    }
/* markInUse -- Mark the specified cell face as contoured. This is necessary to
 *  prevent infinite processing of closed contours. see also:  faceInUse */
static
void    markInUse (grid, point, face)
GRID    *grid;
POINT   *point;
uchar   face;
    {
    face = (point->bearing + face) % 4;
    switch (face)
        {
        case NORTH:
        case SOUTH:
            grid->map[MXY_to_L (grid,
                point->x, point->y + (face == SOUTH ? 1 : 0))] |= NS_MAP;
            break;
        case EAST:
        case WEST:
            grid->map[MXY_to_L (grid,
                point->x + (face == EAST ? 1 : 0), point->y)] |= EW_MAP;

            break;
        }
    }
/* faceInUse -- Determine if the specified cell face has been marked as
 * contoured. This is necessary to prevent infinite processing of closed
 * contours. see also: markInUse */
static
uchar   faceInUse (grid, point, face)
GRID    *grid;
POINT   *point;
uchar   face;
    {
    uchar   r;
    face = (point->bearing + face) % 4;
    switch (face)
        {
        case NORTH:
        case SOUTH:
            r = grid->map[MXY_to_L (grid,
                    point->x, point->y + (face == SOUTH ? 1 : 0))] & NS_MAP;
            break;
        case EAST:
        case WEST:
            r = grid->map[MXY_to_L (grid,
                    point->x + (face == EAST ? 1 : 0), point->y)] & EW_MAP;
            break;
        }
    return r;
    }


/* initPoint -- Initialize the contour point list.
 * see also: savePoint, lastPoint  */
static
void    initPoint (grid)
GRID    *grid;
    {
    grid->count = 0;
    }

/* lastPoint -- Generate the actual contour line from the contour point list.
 *  see also:  savePoint, initPoint  */
static
void    lastPoint (grid)
GRID    *grid;
    {
    if (grid->count)
        Polyline (grid->count, grid->list);
    }

/* savePoints -- Add specified point to the contour point list.
 *  see also:  initPoint, lastPoint */
static
uchar   savePoint (grid, point, level)
GRID    *grid;
POINT   *point;
float   level;
    {
    float   last, next;
    float   x, y, ds;
    char    s[80];

    static  int cnt = 0;

    last = getDataPoint (grid, point, SAME);
    next = getDataPoint (grid, point, NEXT);

    /* Are the points the same value? */
    if (last == next)
        {
        fprintf (stderr, "(%2d, %2d, %d)  ", point->x, point->y,
            point->bearing);
        fprintf (stderr, "%8g  %8g  ", last, next);
        fprintf (stderr, "potential divide-by-zero!\n");
        return 0;
        }
    x = (float) point->x;
    y = (float) point->y;

    ds = (float) ((last - level) / (last - next));


    switch (point->bearing)
        {
        case EAST :                 y += ds;        break;
        case NORTH: x += ds;        y += 1.0;       break;
        case WEST : x += 1.0;       y += 1.0 - ds;  break;
        case SOUTH: x += 1.0 - ds;                  break;
        }

    /* Update to contour point list */
    grid->list[grid->count].x = x / (float) (grid->dim_x - 1);
    grid->list[grid->count].y = y / (float) (grid->dim_y - 1);

    /* Add text label to contour line. */
    if (!(cnt++ % 11))
        {
        sprintf (s, grid->format, level);
        Text (s, grid->list[grid->count].x, grid->list[grid->count].y);
        }
    /* Update counter */
    grid->count++;

    return 1;
    }
/* getDataPoint -- Return the value of the data point in specified corner of
 * specified cell (the 'point' parameter contains the address of the
 *  top-left corner of this cell). */
static
float   getDataPoint (grid, point, corner)
GRID    *grid;
POINT   *point;
uchar   corner;
    {
    ushort  dx, dy;
    ushort  offset;

    switch ((point->bearing + corner) % 4)
        {
        case SAME    :  dx = 0; dy = 0; break;
        case NEXT    :  dx = 0; dy = 1; break;
        case OPPOSITE:  dx = 1; dy = 1; break;
        case ADJACENT:  dx = 1; dy = 0; break;

        }
    offset = XY_to_L (grid, point->x + dx, point->y + dy);
    if ((short) (point->x + dx) >= grid->dim_x ||
        (short) (point->y + dy) >= grid->dim_y ||
        (short) (point->x + dx) < 0 ||
        (short) (point->y + dy) < 0)
        {
        return  NaN;
        }
    else
        return grid->data[offset];
    }






[LISTING TWO]


#include    <stdio.h>

typedef struct
    {
    float   x, y;
    }   LIST;
void    Polyline (n, list)
int     n;
LIST    *list;
    {
    short   x0, x1, y0, y1;
    if (n < 2)
        return;
    printf ("newpath\n");
    printf ("%.6f  %.6f moveto\n", list->x, 1.0 - list->y);
    list++;

    while (--n)
        {
        printf ("%.6f  %.6f lineto\n", list->x, 1.0 - list->y);
        list++;
        }
    printf ("stroke\n\n");

    }
void    Text (s, x, y)
char    *s;
float   x, y;
    {
    printf ("%.6f  %.6f moveto (%s) show\n", x, 1.0 - y, s);
    }
void    psOpen ()
    {
    printf ("%%!\n");
    printf ("save\n");
    printf ("\n");
    printf ("/Helvetica findfont 0.015 scalefont setfont\n");
    printf ("\n");

    printf ("72 252 translate\n");
    printf ("468 468 scale\n");
    printf ("0.001 setlinewidth\n");
    printf ("\n");
    printf ("newpath\n");
    printf (" 0 0 moveto\n");
    printf (" 0 1 lineto\n");
    printf (" 1 1 lineto\n");
    printf (" 1 0 lineto\n");
    printf (" closepath\n");
    printf ("stroke\n");
    printf ("clippath\n");
    printf ("\n");
    printf ("0.00001 setlinewidth\n");
    printf ("\n");
    }
void    psClose ()
    {
    printf ("restore\n");
    printf ("showpage\n");






[LISTING THREE]


#include    <stdio.h>
#include    <math.h>

float   data[400];

void    main (argc, argv)
int     argc;
char    **argv;
    {
    float   *s;
    int     i, j;
    double  x, y, r1, r2;

    s = data;
    for (i = 0, s = data; i < 20; i++)
        for (j = 0; j < 20; j++)
            {
            x = ((double) i - 9.5) / 4.0;
            y = ((double) j - 9.5) / 4.0;

            *s++ = (float) (10.0 * cos(x) * cos(y));
            }
    psOpen ();
    Contour (data, 20, 20, 2.0);

    psClose ();
    }


Copyright © 1992, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.