Andrew is a lecturer in the department of computer science at the University of Melbourne, Australia. He can be reached at ad@cs.mu.oz.au.
One of the nice things about the World Wide Web (WWW) is the ease with which you can add images to WWW pages. However, much more can be done with pictures than simply using them for decoration. For instance, you might want to use "clickable" graphics, where something happens when users click on an image. The HTML code in Example 1, for instance, specifies a page that uses a picture (the Marx Brothers in marx.gif) as a link to the document marx-info.html. On your screen, this looks something like Figure 1. When users click anywhere on the picture, the hypertext (or "hyper-picture") link is followed to marx-info.html (see Listing One), which looks like Figure 2 on your screen.
The drawback to this example is that clicking only causes one thing to happen, no matter where on the picture the cursor is located. A more interesting capability would be a clickable image with "hot spots" linking different regions in a picture to different actions. For instance, in the Marx Brothers picture, you could make the faces of Harpo, Groucho, and Chico hot spots, so that when someone clicks on them, only information about that individual will appear. Other common uses of hot spots are interactive maps, where clicking on a particular building brings up relevant information, or role-playing games, where clicking on an item (a whiskey bottle, a corpse, and so on) returns a clue.
The two basic approaches to implementing clickable images with hot spots in HTML involve either forms or environment variables.
When implementing clickable images using forms, the key is to use a form INPUT tag with an IMAGE attribute. Example 2 shows a small WWW page that uses these features. On screen, this looks like Figure 3. The code between FORM and </FORM> defines the form, with the METHOD attribute determining the means by which the form information is sent to the WWW server. The ACTION attribute names the program that will be invoked. The NAME attribute of the INPUT tag specifies an arbitrary string used to identify the picture. The TYPE attribute states that the input of the form is from an image. SRC gives the location of the marx.gif picture.
The form in Figure 3 looks somewhat odd because the usual Send or Transmit button is missing. Instead, the form details are sent when the cursor is over the image and the mouse is clicked. As described in my article "Coding with HTML Forms," (DDJ, June 1995), the details are output as a string with the format name=value&name=value&..., where name is the name of the form's data-input field, and value is its associated data.
In this example, the only data-entry "field" is the picture named "Marx." However, since the input type is image, the two fields Marx.x=<X-character> and Marx.y=<Y-character> are transmitted. X and Y are the coordinates of the cursor over the image when the mouse was clicked. The axes for an image start at (0,0) in the top-left corner, with X increasing across and Y increasing downwards. The coordinates of various parts of an image can be obtained using most graphics packages; xv is the easiest to use under UNIX.
When the form string arrives at the application, it can be manipulated using the techniques described in my previous article. In this example, however, qgp echoes the field values; see Figure 4. A more-useful application would return different WWW pages depending on the (X,Y) coordinates, perhaps using the NAME attribute to access a file holding relevant hot-spot information for marx.gif.
In the early days of HTML, many browsers and servers did not support forms. This is rapidly changing, making the entry of information through WWW pages much easier. However, the full power of forms is not necessary for clickable images since the data passed to the application is relatively simple.
In non-forms-based HTML programming, two environment variables are commonly utilized: PATH_INFO and QUERY_ STRING. Many other environment variables are also available; see http://hoohoo.ncsa.uiuc.edu/cgi/primer.html and http:// hoohoo.ncsa.uiuc.edu/cgi/env.html for a complete list.
The application which services clickable images with hot spots uses the variables PATH_INFO, QUERY_STRING, and PATH_ TRANSLATED, and it is actually possible to manage without the (explicit) use of QUERY_STRING. Listing Two presents the WWW page that acts as the interface to this application. The file is also at http://www.cs.mu.oz.au/~ad/code/visuals/mxi.html. In a browser, it looks like Figure 5.
The target location (the string assigned to href) is composed of two parts: http://www.cs.mu.oz.au/cgi-bin/mapper, the application location, and /~ad/code/visuals/marx.map, a string that will be assigned to PATH_INFO by the WWW server. The server can divide the string because it "knows" that cgi-bin is the WWW application directory. This knowledge is stored in the configuration file for the http daemon (called http.conf) as the line: Exec /cgi-bin/* /local/dept/wwwd/scripts/*. This acts as a kind of rewrite rule that determines the actual location of the application /local/dept/wwwd/scripts/mapper. The PATH_INFO value should be a string representing a partial path to a file, although it could be any kind of string without spaces. In this example, the partial path is ~ad/code/visuals, the path from the home directory of the author. The filename is marx.map.
The partial path in PATH_INFO is automatically translated into a full path and assigned to the PATH_TRANSLATED environment variable. The translation strategy is again determined by rewrite rules in http.conf. For partial paths starting at a home directory, the relevant rule is UserDir www-public, which states the location of WWW directories beneath each individual's home directory. Thus, PATH_TRANSLATED is assigned /home/staff/ad/ www-public/code/visuals/marx.map. This is the file holding the hot-spot information for the marx.gif picture.
When the image in Figure 5 is clicked upon, information is sent to the server in the form of a long URL. The URL consists of the string assigned to href, with a string of the form ?X,Y appended to the end.
X and Y will be the characters representing the (X,Y) coordinate where the image was clicked, using the coordinate system just described. The characters are appended by the inclusion of the ismap tag in the IMG attribute.
To summarize, the string delivered to the WWW server has the form application-name/filename-with-partial-path?X,Y. The server processes the X,Y tail of the string in two ways: It is assigned to the QUERY_STRING environment variable, and the string is space separated into parts and assigned to the C command-line arguments argv[1], argv[2],.... argv[0] always contains the application name. In this example, X,Y contains no spaces, so it is completely assigned to argv[1].
Once the command line is built, the server uses it to invoke the application, and also makes the various environment variables available to the executing program. Thus, for the HTML code in Listing Two (displayed in Figure 5), the mapper application will be called with argv[1] assigned some X,Y string (for example, "152,149"). Mapper will also have PATH_INFO and PATH_TRANSLATED available, and will therefore be able to access the hot-spot information file marx.map. It could also read the X,Y string through QUERY_STRING, but this is unnecessary.
Mapper.c is a C program which implements clickable images with hot spots. It has three main duties:
Mapper.c has similar functionality to imagemap.c except that mapper.c assumes that the hot-spot information filename can be located solely by examining PATH_TRANSLATED. Imagemap.c also looks in a predefined configuration file that holds other hot-spot information filenames. The extra functionality is useful but inessential for this discussion, so the code for it has been excluded from mapper.c.
The other changes in mapper.c are cosmetic: Prototype declarations have been included, extra comments have been added, and the code has been restructured to use more functions.
Mapper.c uses the hot-spot information-file format designed for imagemap.c. Using the terminology of that program, hot-spot information files are called image-map files. An image map consists of lines of the form <hot-spot-shape-type> <associated-URL> <shape outline co-ordinates>. There are five hot-spot shape types: circle, poly (polygons), rect (rectangles), point, and default. For instance, marx.map contains the information in Example 3. Note that the # lines are comments. The default line specifies what URL should be returned when a clicked (X,Y) point is in none of the hot spots. The three circles correspond to the faces of Harpo, Groucho, and Chico. A circle is specified by a center and another coordinate that indirectly gives the radius. The two poly hot spots match the suitcases labeled "Blondes" and "Gags" in marx.gif.
A complication is the interaction between the default and point hot-spot types. A point hot spot specifies an (X,Y) coordinate, which is compared with the clicked point. If there are several point hot spots, then the one nearest to the clicked point is deemed active. You cannot have a point hot spot and use default in the same image, since they each achieve the same thing.
For more details on the image-map file format, see http:// hoohoo.ncsa.uiuc.edu/docs/setup/admin/NewImagemap.html.
The mapper.c program first reads the PATH_TRANSLATED environment variable that contains the location of the image-map file for the clickable picture. get_map() does this and, if successful, assigns the string to map. Errors are dealt with by calling servererr(), which prints a WWW error page such as the one in Figure 6 and then causes the application to terminate.
If the image-map file can be opened, then the X and Y values in the argv[1] string are extracted by get_clickpt(), which stores them in the clickpt array.
process_map() has three jobs: It reads in and parses the image-map file, locates the (X,Y) coordinate in one of the hot spots, and transmits the associated URL to the user.
It reads the image-map file a line at a time inside a While loop, skipping # comment lines and blank lines. The first call to get_word() stores the hot-spot shape type in the type array, the second call stores its URL in url.
The default case has its associated URL stored in deflt unless any point hot spots have already been processed.
get_coords() reads in the series of coordinates that specify a hot-spot outline and stores them in the coords array.
A series of strcmp() tests follow to determine the hot-spot shape type. Then a function is called to test if the clicked point is inside the shape area specified by the coordinates in the coords array.
The test for a point hot spot is different because the final choice of point hot spot must wait until all have been tested against the clicked point. In the meantime, the URL of the hot spot currently closest to the clicked point is stored in deflt.
The URL associated with the chosen hot spot is sent to the user by calling sendmesg(). This function prints the URL of a WWW page instead of its text. This is intercepted by the server, which sends the actual document to the user. URLs are printed using the format Location: <URL> followed by a blank line.
sendmesg() is complicated by its ability to deal with full URLs (for example, http://www.cs.mu.oz.au/~ad/index.html) and partial URLs (/~ad/code/visuals/mgm.html). The former is printed immediately, the latter is expanded into a full URL by prefixing it with the server's host name (for instance, www.cs.mu.oz.au) to make http://www.cs.mu.oz.au/~ad/code/visuals/mgm.html. Details about the printing of URLs can be found at http:// hoohoo.ncsa.uiuc.edu/cgi/primer.html and http://hoohoo.ncsa .uiuc.edu/cgi/env.html.
To illustrate the implementation and use of clickable images, let's start with the example in Figure 5 (the corresponding code is shown in Listing Two and at http://www.cs.mu.oz.au/ ~ad/code/visuals/mxi.html). The hot spots for the image are specified in marx.map as the faces of Harpo, Groucho, and Chico, and the "Blondes" and "Gags" suitcases. Marx.map also includes URLs for each hot spot and for the default case.
When Groucho's face is clicked upon, Figure 7 appears on the screen, which is defined at http://www.cs.mu.oz.au/~ad/ code/visuals/groucho.html. Similarly, clicking on the "Gags" suitcase produces Figure 8, which is at http://www.cs.mu.oz.au/ ~ad/code/visuals/gags.html. The default case is linked to http://www.cs.mu.oz.au/~ad/code/visuals/mgm.html, displayed in Figure 9.
The layout of the various pages is meant to give the impression that the picture stays on the screen continuously while the text around it changes.
Clickable images with hot spots are a helpful WWW technique, especially for guide books (see, for example, the University of California Museum of Paleontology pages starting at http:// ucmp1.berkeley.edu/), role-playing games, and maps (for instance, the Internet resources "map" at http://www.ncsa.uiuc.edu/ SDG/Software/Mosaic/Demo/metamap.html). They can also enliven contents pages (such as with the HotWired WWW pages at http://www.hotwired.com).
Figure 1: WWW page with image.
Figure 2: WWW page linked to Figure 1.
Figure 3: Page generated by HTML code in Example 2.
Figure 4: Echoing the field values.
Figure 5: WWW page which acts as the interface to an application.
Figure 6: Typical WWW error page.
Figure 7: Clicking on Groucho's face generates this screen.
Figure 8: Image produced by clicking on the "Gags" suitcase.
Figure 9: Clicking on the default link generates this screen.
Example 1: HTML code specifying a page that uses a picture as a link.label.
Example 2: Typical WWW page.
Example 3: Using hot-spot shape types.
Copyright © 1995, Dr. Dobb's Journal
<html>
<head>
<TITLE>The Marx Brothers (image as anchor label)</TITLE>
</head>
<body>
<H1>The Marx Brothers (image as anchor label)</H1>
<br>
<img src="gball.gif" alt=""> Click on the picture for more
on the Marx Brothers. <p>
<a href="marx-info.html"><img src="marx.gif" alt=""></a> <p>
<hr>
<address><a href="http://www.cs.mu.oz.au/~ad">Andrew Davison</a></address>
</body>
</html>
<html>
<head>
<TITLE>The Marx Brothers (using forms)</TITLE>
</head>
<body>
<H1>The Marx Brothers (using forms)</H1>
<br>
<img src="gball.gif" alt=""> Click on the picture to find
out more: <p>
<FORM METHOD="POST"
ACTION="http://www.cs.mu.oz.au/cgi-bin/qgp">
<INPUT NAME="Marx" TYPE="image"
SRC="http://www.cs.mu.oz.au/~ad/code/visuals/marx.gif"> <p>
</FORM>
<hr>
<address><a href="http://www.cs.mu.oz.au/~ad"
>Andrew Davison</a></address>
</body>
</html>
# image map for Marx Brothers image
default /~ad/code/visuals/mgm.html
# left head
circle /~ad/code/visuals/harpo.html 52,33 52,10
# middle head
circle /~ad/code/visuals/groucho.html 142,33 142,10
#right head
circle /~ad/code/visuals/chico.html 230,46 230,26
# blondes suitcase
poly /~ad/code/visuals/blondes.html 19,74 92,62 101,101 31,109
# gags suitcase
poly /~ad/code/visuals/gags.html 219,86 309,44 310,87 251,110
Listing One
<html>
<head>
<TITLE>The Marx Brothers</TITLE>
</head>
<body>
<H1>The Marx Brothers</H1>
A family of Jewish-American comics whose zany
humour convulsed minority audiences in its time and influenced later
comedy writing to an enormous extent. <p>
<h2>Groucho (1890-1977)</h2>
(Julius Marx) had a painted moustache, a cigar, a
loping walk and the lion's share of the wisecracks. <p>
<h2>Harpo (1888-1964)</h2>
(Adolph Marx) was a child-like mute who also played a harp. <p>
<h2>Chico (1886-1961)</h2>
(Leonard Marx) played the piano eccentrically and
spoke with an impossible Italian accent. <p>
<h2>The Other Brothers</h2>
Aside from Chico, Groucho and Harpo (shown above),
there were two other brothers: <i>Gummo (1893-1977)</i>
(Milton Marx) and <i>Zeppo (1901-1979)</i> (Herbert Marx)
who left the team early on. <p>
<hr>
<address><a href="mx0.html">To Start</a></address>
</body>
</html>
Listing Two
<html>
<head>
<TITLE>The Marx Brothers</TITLE>
</head>
<body>
<H1>The Marx Brothers</H1>
<br>
<img src="gball.gif" alt=""> Click on the picture to find out more: <p>
<a href="http://www.cs.mu.oz.au/cgi-bin/mapper/~ad/code/visuals/marx.map">
<img src="marx.gif" alt="" ismap></a> <p>
<hr>
<h2>The Marx Brothers</h2>
A family of Jewish-American comics whose zany
humour convulsed minority audiences in its time and influenced later
comedy writing to an enormous extent. <p>
Aside from Chico, Groucho and Harpo (shown above),
there were two other brothers: <i>Gummo (1893-1977)</i>
(Milton Marx) and <i>Zeppo (1901-1979)</i> (Herbert Marx)
who left the team early on. <p>
<hr>
<address><a href="http://www.cs.mu.oz.au/~ad">Andrew Davison</a></address>
</body>
</html>
Listing Three
/* Simplified version of mapper 1.2
** Based on work by: Kevin Hughes,
** Eric Haines, Rob McCool, Chris Hyams, Rick Troth,
** Craig Milo Rogers, Carlos Varela
** Original version at
** http://hoohoo.ncsa.uiuc.edu/docs/setup/admin/NewImagemap.html
** Andrew Davison (ad@cs.mu.oz.au) January 1995
** Available at http://www.cs.mu.oz.au/~ad/code/visuals/mapper.c
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#define MAXLINE 500 /* max length of line in image map file */
#define MAXVERTS 100 /* max num of coords in a shape */
#define NUMLEN 10 /* max num of digits in a X or Y number */
#define LF 10
#define X 0
#define Y 1
void get_map(char map[]);
void get_clickpt(char *arg, double clickpt[]);
void process_map(FILE *fp, double clickpt[]);
void get_word(char *input, int *pi, char *word);
void get_num(char *input, int *pi, char *num);
void get_coords(char *input, int *pi, double coords[][2], int size, FILE *fp);
void servererr(char *msg);
void sendmesg(char *url);
double sdist_apart(double clickpt[], double coords[][2]);
int clickpt_in_rect(double clickpt[], double coords[][2]);
int clickpt_in_circle(double clickpt[], double coords[][2]);
int clickpt_in_poly(double clickpt[], double pgon[][2]);
int main(int argc, char *argv[])
{
char map[MAXLINE]; /* name of image map file with full path */
double clickpt[2]; /* for the (X,Y) coord clicked upon */
FILE *fp;
if (argc != 2)
servererr("Wrong number of arguments, client may not support ISMAP.");
get_map(map);
if((fp = fopen(map,"r")) == NULL)
servererr(strcat("Couldn't open image map file:", map));
else {
get_clickpt(argv[1], clickpt);
process_map(fp, clickpt);
fclose(fp);
}
return 0;
}
void get_map(char map[])
/* obtain the image map file name with a full UNIX path */
{
char *name; /* name of image map file with partial path */
name = getenv("PATH_INFO");
if((!name) || (!name[0]))
servererr("No image map name given. Please read the
<A HREF=\"http://hoohoo.ncsa.uiuc.edu/docs/setup/admin/NewImagemap.html\">
instructions</A>.<P>");
name++; /* ignore first '/' */
/* if the name contains a '/' then it represents a partial UNIX path */
if (strchr(name,'/'))
strcpy(map, getenv("PATH_TRANSLATED"));
else
servererr("Map name must include a partial UNIX path.");
}
void get_clickpt(char *arg, double clickpt[])
/* extract the (X,Y) coord clicked upon */
{
char *t;
if((t = strchr(arg,',')) == NULL)
servererr("Your client doesn't support image mapping properly.");
*t++ = '\0';
clickpt[X] = (double) atoi(arg);
clickpt[Y] = (double) atoi(t);
}
void process_map(FILE *fp, double clickpt[])
/* parse the image map file, locate clickpt inside a hot shape,
invoke the associated URL */
{
char input[MAXLINE]; /* a line from the image map file */
char type[MAXLINE]; /* type of hot spot shape */
char url[MAXLINE]; /* URL associated with shape type */
char deflt[MAXLINE]; /* the URL for the default case */
double coords[MAXVERTS][2]; /* the coordinates of a shape */
int num_ptshapes = 0; /* number of point hot spots used */
double dist, min_dist; /* for nearest point hot spot calc */
int i;
while((fgets(input, MAXLINE, fp)) != NULL) {
i = 0;
if((input[i] == '#') || (!input[i]))
continue;
get_word(input, &i, type);
while(isspace(input[i]))
i++;
get_word(input, &i, url);
if((strcmp(type,"default") == 0) && (num_ptshapes == 0)) {
strcpy(deflt,url);
continue;
}
get_coords(input, &i, coords, MAXVERTS, fp);
if(strcmp(type,"poly") == 0) /* poly type */
if(clickpt_in_poly(clickpt,coords))
sendmesg(url);
if(strcmp(type,"circle") == 0) /* circle type */
if(clickpt_in_circle(clickpt,coords))
sendmesg(url);
if(strcmp(type,"rect") == 0) /* rect type */
if(clickpt_in_rect(clickpt,coords))
sendmesg(url);
if(strcmp(type,"point") == 0) { /* point type */
dist = sdist_apart(clickpt, coords);
/* If first point hot spot, or the nearest, set the default. */
if ((num_ptshapes == 0) || (dist < min_dist)) {
min_dist = dist;
strcpy(deflt,url);
}
num_ptshapes++;
}
}
if(deflt[0])
sendmesg(deflt);
else
servererr("No default specified.");
}
void get_word(char *input, int *pi, char *word)
/* extract a word from an input line */
{
int i;
for(i=0; ((!isspace(input[*pi])) && (input[*pi])); i++) {
word[i] = input[*pi];
(*pi)++;
}
word[i] = '\0';
}
void get_num(char *input, int *pi, char *num)
/* extract a number (as characters) from an input line */
{
int i = 0;
while ((isspace(input[*pi])) || (input[*pi] == ',')) /* find char */
(*pi)++;
while (isdigit(input[*pi]))
num[i++] = input[(*pi)++];
num[i] = '\0';
}
void get_coords(char *input, int *pi, double coords[][2], int size, FILE *fp)
/* extract the (X,Y) coords from an input line,
and store them in the coords array */
{
char num[MAXLINE]; /* a X or Y part of a coord */
int k=0;
while ((input[*pi]) && (k < size)) {
get_num(input, pi, num);
if (num[0] != '\0')
coords[k][X] = (double) atoi(num); /* X part of a coord */
else
break;
get_num(input, pi, num);
if (num[0] != '\0') {
coords[k][Y] = (double) atoi(num); /* Y part of a coord */
k++;
}
else {
fclose(fp);
servererr("Missing Y value in a co-ordinate.");
}
}
if (k < size)
coords[k][X] = -1; /* sentinel */
else
coords[k-1][X] = -1; /* overwrite last coord */
}
/* HTML print utilities */
void servererr(char *msg)
{
printf("Content-type: text/html%c%c",LF,LF);
printf("<title>Mapping Server Error</title>");
printf("<h1>Mapping Server Error</h1>");
printf("This server encountered an error:<p>");
printf("%s", msg);
exit(-1);
}
void sendmesg(char *url)
{
if (strchr(url, ':')) /* It is a full URL */
printf("Location: ");
else /* It is a virtual URL */
printf("Location: http://%s", getenv("SERVER_NAME"));
printf("%s%c%c",url,LF,LF);
exit(0);
}
/* clickpt locating functions */
double sdist_apart(double clickpt[2], double coords[MAXVERTS][2])
/* Find the square of the distance between the click point
and the coords of a point hot spot.
Don't need to take square root */
{
return ((clickpt[X] - coords[0][X]) * (clickpt[X] - coords[0][X])) +
((clickpt[Y] - coords[0][Y]) * (clickpt[Y] - coords[0][Y]));
}
int clickpt_in_rect(double clickpt[2], double coords[MAXVERTS][2])
/* is the clickpt inside a rectangle? */
{
return ((clickpt[X] >= coords[0][X] && clickpt[X] <= coords[1][X]) &&
(clickpt[Y] >= coords[0][Y] && clickpt[Y] <= coords[1][Y]));
}
int clickpt_in_circle(double clickpt[2], double coords[MAXVERTS][2])
/* is the clickpt inside a circle? */
{
int radius1, radius2;
radius1 = ((coords[0][Y] - coords[1][Y]) * (coords[0][Y] - coords[1][Y])) +
((coords[0][X] - coords[1][X]) * (coords[0][X] - coords[1][X]));
radius2 = ((coords[0][Y] - clickpt[Y]) * (coords[0][Y] - clickpt[Y])) +
((coords[0][X] - clickpt[X]) * (coords[0][X] - clickpt[X]));
return (radius2 <= radius1);
}
int clickpt_in_poly(double clickpt[2], double pgon[MAXVERTS][2])
/* is the clickpt inside a polygon? */
{
int i, numverts, inside_flag, xflag0;
int crossings;
double *p, *stop;
double tx, ty, y;
for (i = 0; pgon[i][X] != -1 && i < MAXVERTS; i++)
;
numverts = i;
crossings = 0;
tx = clickpt[X];
ty = clickpt[Y];
y = pgon[numverts - 1][Y];
p = (double *) pgon + 1;
if ((y >= ty) != (*p >= ty)) {
if ((xflag0 = (pgon[numverts - 1][X] >= tx)) ==
(*(double *) pgon >= tx)) {
if (xflag0)
crossings++;
}
else {
crossings += (pgon[numverts - 1][X] - (y - ty) *
(*(double *) pgon - pgon[numverts - 1][X]) /
(*p - y)) >= tx;
}
}
stop = pgon[numverts];
for (y = *p, p += 2; p < stop; y = *p, p += 2) {
if (y >= ty) {
while ((p < stop) && (*p >= ty))
p += 2;
if (p >= stop)
break;
if ((xflag0 = (*(p - 3) >= tx)) == (*(p - 1) >= tx)) {
if (xflag0)
crossings++;
}
else {
crossings += (*(p - 3) - (*(p - 2) - ty) *
(*(p - 1) - *(p - 3)) / (*p - *(p - 2))) >= tx;
}
}
else {
while ((p < stop) && (*p < ty))
p += 2;
if (p >= stop)
break;
if ((xflag0 = (*(p - 3) >= tx)) == (*(p - 1) >= tx)) {
if (xflag0)
crossings++;
}
else {
crossings += (*(p - 3) - (*(p - 2) - ty) *
(*(p - 1) - *(p - 3)) / (*p - *(p - 2))) >= tx;
}
}
}
inside_flag = crossings & 0x01;
return (insid