Stepping through an image map applet

Spice up any Web site with this reusable Java code

Many a Web site has considered employing Java within its home page. The image map, a mainstay of home pages consisting of a GIF or JPEG image mapped to link to various documents depending upon where a user clicks within the image, lends itself nicely to Java enhancement. In this inaugural Step by Step column, we'll walk you through the creation of a reusable image map applet.

Overview of the problem

The basic idea: Allow for regions to be defined on an image. When the user moves the mouse into the region, another image is displayed. When the user clicks in the region, the browser is driven to a URL associated with that area. This applet allows for two styles of image placement, either over the region or at the bottom of the background image. The difference is a matter of style. Both work well, and the choice will depend on the desired effect.

One suggestion is to use below-background image placement to provide textual information relating to the link; interesting graphical effects can be achieved with Image overlays. If this applet is designed to be as generic as possible, then it can be written and compiled once, and reused many times with differing effects.

Global elements

The basic elements are a background image, a boolean value specifying the image placement, a Vector of Rectangles representing the defined regions, a Vector of images associated with those regions, and a Vector of URLs to which each region should point. We also must keep the index of the current active region.

import java.awt.*;
import java.applet.*;
import java.util.*;
import java.net.*;
public class ImageMap extends Applet {
  Image background;
  boolean overlay;
  Vector areas = new Vector();
  Vector images = new Vector();
  Vector urls = new Vector();
  int current = -1;

Vectors are used so that any number of regions may be defined. A Vector is a dynamically allocated array that grows as needed. This is more efficient than a large array for most cases.

Initialization and parameter collection

The

init()

method does all the work of collecting the parameters. This method is called automatically when the applet first loads.

  public void init() {
    // get the parameters
    background = getImage(getDocumentBase(), getParameter("main"));
    prepareImage(background, this);
    overlay = Boolean.valueOf(getParameter("overlay")).booleanValue();
    String bgcolor = getParameter("bgcolor");
    if (bgcolor != null)
      setBackground (new Color (Integer.parseInt (bgcolor.substring (1), 16)));
    boolean ok = true;
    for (int i = 1; ok; ++i) {
      String areaStr = getParameter("area"+i);
      String imageStr = getParameter("image"+i);
      String urlStr = getParameter("url"+i);
      if ((areaStr != null) && (imageStr != null) && (urlStr != null)) {
        try {
          StringTokenizer str = new StringTokenizer(areaStr);
          Rectangle area = new Rectangle(Integer.parseInt(str.nextToken()),
                                         Integer.parseInt(str.nextToken()),
                                         Integer.parseInt(str.nextToken()),
                                         Integer.parseInt(str.nextToken()));
          Image image = getImage(getDocumentBase(), imageStr);
          URL url = new URL(getDocumentBase(), urlStr);
          areas.addElement(area);
          images.addElement(image);
          urls.addElement(url);
          prepareImage (image, this);
        } catch (MalformedURLException ex) {
          System.out.println ("Invalid format for URL " + i + " : " + urlStr);
        } catch (NoSuchElementException ex) {
          System.out.println ("Invalid format for area " + i + " : " + areaStr);
        } // end try
      } else {
        ok = false;
      } // end if
    } // end for
  } // end init

There are several parameters that we need to collect from the HTML APPLET tag. A typical tag will look like this:

&ltapplet code=ImageMap width=461 height=585>
&ltparam name="main" value="jwbackcolors.gif">
&ltparam name="bgcolor" value="#000000">
&ltparam name="overlay" value="true">
&ltparam name="area1" value="1 3 460 138">
&ltparam name="image1" value="images/jw1.gif">
&ltparam name="url1" value="http://prominence.com/">
&ltparam name="area2" value="8 149 243 188">
&ltparam name="image2" value="jw2.gif">
&ltparam name="url2" value="http://prominence.com/">
&ltparam name="area3" value="256 147 196 26">
&ltparam name="image3" value="images/jw3.gif">
&ltparam name="url3" value="http://prominence.com/">
&ltparam name="area4" value="255 171 205 34">
&ltparam name="image4" value="images/jw4.gif">
&ltparam name="url4" value="http://prominence.com/">
</applet >

The first three parameters define the general features of the image map. The "main" parameter is the background image, and the "bgcolor" is the background color. The "overlay" parameter should have a value of true or false, and determines whether the images are drawn on top of the background image over their respective areas or centered below. We use the type wrapper objects, Boolean and Integer, to convert strings to the primitive types. The Boolean class has a static method, valueOf, that takes a string as a parameter and returns the corresponding Boolean whose booleanValue method we call. The Integer class has a method, parseInt, that takes a string and returns an integer. We want the integer and boolean values as primitive types, not reference types, so we use these classes to perform manipulation of the strings in order to get integer and boolean primitive types.

When we collect the background image into the variable background, we call the applet's prepareImage method with the image as the parameter. This method begins downloading the image at that time; otherwise, the image would not be downloaded until we need to paint it for the first time, causing more flicker. This is a simple optimization that can be employed when images are used.

The rest of the parameters define distinct regions of the image map with a string representing the Rectangle coordinates (x , y, width, height), and the images and URLs associated with each region. The parameter names start with area, image, and url, respectively, and numbers are added to the end, beginning with 1. Since we are using Vectors to store these values, there is no limit on the number of regions that can be defined.

Validity checking makes the applet more robust, and we require that each region have a valid image, URL, and coordinate set. We collect the parameters in logical groups of three, checking to make sure that none of the Strings are null. If any of these are null, we set the ok boolean to false to indicate that it was the last fully correct region in the sequence, and the for loop will terminate. A try .. catch statement is then used to ensure that everything is correct when the parameters are converted to their usable types.

An interesting part here is parsing the area string to collect four integer values. A new StringTokenizer is created based on the parameter. The StringTokenizer class, found in the java.util package, is a handy utility for parsing tokens from strings. By default, the StringTokenizer class considers any white space to be a delimiter, so tokens are contiguous sets of characters that are not white space. The nextToken method is used to extract the next set of characters. We then create a Rectangle object based on the tokens that we extract. If we fail to extract four numbers, execution proceeds to the catch statement, which prints a usage error. We then collect the Image and create the URL. We again call the prepareImage method on each Image to begin its download. The Rectangle, Image, and URL are then added to their respective Vectors, and setup is complete.

Event handling

All of the event handling is taken care of in the

handleEvent

method. We need to check for mouse activity, and there are several events that can occur. If all of the possible mouse events are listed as cases in the switch without break statements between them, then the code found under the last case will be executed if any of these event types happen.

  public boolean handleEvent(Event e) {
    switch(e.id) {
    case Event.MOUSE_ENTER:
    case Event.MOUSE_MOVE:
    case Event.MOUSE_EXIT:
    case Event.MOUSE_DOWN:
    case Event.MOUSE_DRAG:
    case Event.MOUSE_UP:
      int next = -1;
      if (e.id != Event.MOUSE_EXIT) {
        for (int i = 0; i < areas.size (); ++i) {
          Rectangle rect = (Rectangle) areas.elementAt (i);
          if (rect.inside (e.x, e.y)) {
            next = i;
          } // end if
        } // end for
      } // end if
      if (next != current) {
        int temp = current;
        current = next;
        repaintImages (temp, current);
      } // end if
      if ((e.id == Event.MOUSE_DOWN) && (current != -1)) {
        URL url = (URL) urls.elementAt (current);
        getAppletContext().showDocument(url);
      }
      break;
    } // end switch
    return(super.handleEvent(e));
  } // end handleEvent

We first create a temporary integer variable, next, that will be the index of the region with the most recent activity. If the mouse has not exited the applet, then we run through the defined regions to find out where the event occurred. The Event object is instantiated automatically when something happens, and has variables that hold the mouse coordinates at the time of the event. To find out if the event happened in a defined region, we run through the Vector of Rectangles, testing for inclusion. A cast into a temporary Rectangle object makes the code a little cleaner. The Rectangle class comes equipped with a method for testing inclusion of a point, inside, and we use this method to find out which region was affected. We assign the next variable to the index of the Rectangle that contained the point. If no inclusion was found, this variable will remain -1.

If the new region is different from the last currently active region then we need to update the display. If there was an active region before this event, we need to repaint the old region with the background. This will either be the background image or the background color, depending on the image placement style. This is necessary because overlaid images should disappear if a new region receives activity. We call the repaintImages method to handle this. This method takes the old region and the new region, and is explained in the next section. The current variable is then assigned to this new active region.

If the event was a MOUSE_DOWN, then we need to drive the browser to the site associated with the current region. We first check to see that the current variable is not equal to -1, meaning that there is a current region. A cast is then made on the current URL to make the code a little cleaner. Finally, the showDocument method of the Applet class is called to go to that page.

Drawing the images

Once the

handleEvent

method has determined which region, if any, has received activity, we need to refresh the images. Two methods are used to perform this task, and optimizations have been added.

  protected void repaintImages(int last, int current) {
    if (overlay) {
      if (last != -1) {
         Rectangle oldRect = (Rectangle) areas.elementAt (last);
         Image oldImage = (Image) images.elementAt (last);
         repaint (oldRect.x, oldRect.y, oldImage.getWidth (this), 
                  oldImage.getHeight (this));
      }
      if (current != -1) {
         Rectangle newRect = (Rectangle) areas.elementAt (current);
         Image newImage = (Image) images.elementAt (current);
         repaint (newRect.x, newRect.y, newImage.getWidth (this), 
                  newImage.getHeight (this));
      }
    } else {
      repaint (0, background.getHeight (this), size ().width, 
               size ().height - background.getHeight (this));
    } // end if
  } // end repaintImages
  public void paint(Graphics g) {
    g.drawImage (background, 0, 0, this);
    if (current != -1) {
      Image image = (Image) images.elementAt (current);
      if (overlay) {
        Rectangle rect = (Rectangle) areas.elementAt (current);
        g.drawImage (image, rect.x, rect.y, this);
      } else {
        int w = image.getWidth (this), h = image.getHeight (this);
        g.drawImage (image, (size ().width - w) / 2, (size ().height + 
                     background.getHeight (this) - h) / 2, this);
      } // end if
    } // end if
  } // end paint
} // end ImageMap

To perform screen refreshes without flicker, we repaint only the region that has changed. This is not an essential part of the applet, but is a fairly easy optimization that yields a noticeable improvement. If the image placement style is below the background image, then only that region will need to be refreshed. But if the placement style is overlay, two regions will need to be refreshed -- the old and the new. We will need to redraw the background image over the old region's image, and draw the new region's image over the background in its appropriate place. This process is called clipping, and we do it in the repaintImages method.

1 2 Page 1