/************************************************************************************************

Image map with pop-up text displays.

<applet code="IMap.class" width=w height=h>
<param name="mapimage" value="url" or "url,color">
<param name="fgcolor" value="color">
<param name="bgcolor" value="color">
<param name="border" value="size,color">
<param name="font" value="name,style,size">
<param name="margins" value="n,m">
<param name="outline" value="color">
<param name="default" value="url" or "url,target">
<param name="status" value="string">
<param name="shape-1" value="rect,x1,y1,x2,y2" or "circle,x,y,r"
                           or "ellipse,x,y,a,b" or "polygon,x1,y1,x2,y2,...,xn,yn">
<param name="url-1" value="url" or "url,target">
<param name="status-1" value="string">
<param name="text-1-1" value="string" or "indent|string">
<param name="text-1-2" value="string" or "indent|string">
<param name="text-1-3" value="string" or "indent|string">
      ...
<param name="shape-n" value="rect,x1,y1,x2,y2" or "circle,x,y,r"
                           or "ellipse,x,y,a,b" or "polygon,x1,y1,x2,y2,...,xn,yn">
<param name="url-n" value="url" or "url,target">
<param name="status-n" value="string">
<param name="text-n-1" value="string" or "indent|string">
<param name="text-n-2" value="string" or "indent|string">
<param name="text-n-3" value="string" or "indent|string">
      ...
</applet>

************************************************************************************************/

import java.awt.*;
import java.awt.image.*;
import java.net.*;
import java.util.*;
import java.applet.Applet;

/************************************************************************************************
 The IMapArea class defines the area shape, position and text for the the pop-up box.
************************************************************************************************/

class IMapArea {

 // Constants:

 private final static int RECT    = 1;           // Shape types.
 private final static int ELLIPSE = 2;
 private final static int POLY    = 3;

 // Fields:

 private int       shape;                        // This area's shape, one of:
 private Rectangle rect;                         //   rectangle
 private int       x, y, a, b;                   //   ellipse
 private Polygon   poly;                         //   polygon

 public URL       url    = null;                 // URL to link to when clicked.
 public String    target = null;                 // Target window.
 public String    status = null;                 // Status window message.

 private Vector   text   = new Vector(10, 5);    // Text line array.
 private Vector   indent = new Vector(10, 5);    // Indent for each text line.
 private int      count  = 0;                    // Number of text lines.

 public  int      width  = 0;                    // Width of text box.
 public  int      height = 0;                    // Height of text box.

 // Constructors:

 // Create an area of the appropriate shape.

 public IMapArea(Rectangle rect) {

   this.shape = RECT;
   this.rect  = rect;
 }

 public IMapArea(int x, int y, int a, int b) {

   this.shape = ELLIPSE;
   this.x = x;
   this.y = y;
   this.a = Math.abs(a);
   this.b = Math.abs(b);
 }

 public IMapArea(Polygon poly) {

   this.shape = POLY;
   this.poly = poly;
 }

 // Methods:

 // Determine the area shape.

 public boolean isRect() {

   if (this.shape == RECT)
     return(true);

   return(false);
 }

 public boolean isEllipse() {

   if (this.shape == ELLIPSE)
     return(true);

   return(false);
 }

 public boolean isPoly() {

   if (this.shape == POLY)
     return(true);

   return(false);
 }

 // Return the area as a shape object with the appropriate coordinates, useful for drawing.

 public Rectangle getRect() {

   if (this.isRect())
     return(this.rect);

   return(new Rectangle());
 }

 public Rectangle getEllipse() {

   if (this.isEllipse())
     return(new Rectangle(this.x - this.a, this.y - this.b, this.a * 2, this.b * 2));

   return(new Rectangle());
 }

 public Polygon getPoly() {

   if (this.isPoly())
     return(this.poly);

   return(new Polygon());
 }

 // Find the smallest rectangle that contains the shape.

 public Rectangle getBoundingBox() {

   Rectangle rect;

   rect = new Rectangle();
   if (this.isRect())
     rect = this.rect;
   if (this.isEllipse())
     rect = this.getEllipse();
   if (this.isPoly())
     rect = this.poly.getBoundingBox();
   return(rect);
 }

 // Set the URL to link to.

 public void setURL(URL url) {

   this.url = url;
 }

 // Set the URL and target frame or window to link to.

 public void setURL(URL url, String target) {

   this.url = url;
   this.target = target;
 }

 // Set the message to display in the status window when this link is active.

 public void setStatusMsg(String msg) {

   this.status = msg;
 }

 // Determine if a given point is inside the area.

 public boolean inside(int x, int y) {

   double v;

   if (this.isRect())
     return(this.rect.inside(x, y));

   if (this.isEllipse()) {
     x -= this.x;
     y -= this.y;
     if (Math.abs(x) > this.a || Math.abs(y) > b)
       return(false);
     v = (int) ((double) this.b * Math.sqrt(1.0 - (double) (x * x) / (double) (this.a * this.a)));
     if (Math.abs(y) <= v)
       return(true);
   }

   if (this.isPoly())
     return(this.poly.inside(x, y));

   return(false);
 }

 // Add a line of text with the specified indentation.

 public void addText(int n, String s) {

   this.indent.addElement(new Integer(n));
   this.text.addElement(s);
   this.count++;
 }

 // Get a line of text.

 public String getText(int i) {

   if (i >= 0 && i < this.count)
     return((String) this.text.elementAt(i));
   else
     return((String) null);
 }

 // Get amount of indentation for a line of text.

 public int getIndent(int i) {

   if (i >= 0 && i < this.count)
     return(Integer.parseInt(this.indent.elementAt(i).toString()));
   else
     return(0);
 }

 // Get number of text lines.

 public int getCount() {

   return(this.count);
 }
}

/************************************************************************************************
 Main applet code.
************************************************************************************************/

public class IMap extends Applet {

 // Parameters and defaults.

 Image   mapImage   = null;           // Map image.
 Color   pgColor    = Color.white;    // Applet background color.
 Color   fgColor    = Color.black;    // Text color.
 Color   bgColor    = Color.white;    // Text box background color.
 int     bdSize     = 0;              // Text box border size.
 Color   bdColor;                     // Text box border color.
 String  fontName   = "Dialog";       // Text font.
 int     fontStyle  = Font.PLAIN;     // Text font style.
 int     fontSize   = 12;             // Text font size.
 int     hrznMargin = 10;             // Horizontal margin for text.
 int     vertMargin = 10;             // Verticle margin for text.
 boolean olFlag     = false;          // Area outline flag.
 Color   olColor    = Color.black;    // Area outline color.
 URL     defaultUrl = null;           // Default image map link.
 String  target     = null;           // Target window or frame for the default link.
 String  status     = null;           // Status window message for the default link.

 // Global variables.

 Vector areas = new Vector(10, 5);    // A list of all the defined areas.

 IMapArea area;                       // The currently active area.
 int      activeArea = -1;

 Font font;                           // Font size values.
 int  xAdvance;
 int  yAscent;
 int  yHeight;

 Dimension offDimension;              // Off-screen graphic for drawing.
 Image     offImage;
 Graphics  offGraphics;

 // Applet information.

 public String getAppletInfo() {

   return("IMap version 1.5\n\nCopyright 1997 by Mike Hall");
 }

 public void init() {

   String s, t, u;
   StringTokenizer st;
   URL url;
   int i, j;
   Graphics g;
   FontMetrics fm;
   int n;

   // Take credit.

   System.out.println("IMap version 1.5, Copyright 1997 by Mike Hall.");

   // Get the map image and optional background color and start loading the image.

   try {
     s = getParameter("mapimage");
     if (s != null) {
       st = new StringTokenizer(s, ",");
       mapImage = getImage(new URL(getDocumentBase(), st.nextToken()));
       prepareImage(mapImage, this);
       if (st.hasMoreTokens())
         pgColor = getColorParm(st.nextToken());
     }
   }
   catch (Exception e) {}

   // Get the pop-up text box colors.

   try {
     s = getParameter("fgcolor");
     if (s != null)
       fgColor = getColorParm(s);
   }
   catch (Exception e) {}
   bdColor = fgColor;
   try {
     s = getParameter("bgcolor");
     if (s != null)
       bgColor = getColorParm(s);
   }
   catch (Exception e) {}

   // Get the pop-up text box border parameters.

   try {
     s = getParameter("border");
     if (s != null) {
       st = new StringTokenizer(s, ",");
       if ((n = Integer.parseInt(st.nextToken())) > 0)
         bdSize = n;
       if (st.hasMoreTokens())
         bdColor = getColorParm(st.nextToken());
     }
   }
   catch (Exception e) {}

   // Get the text box font.

   try {
     s = getParameter("font");

     // Font name.

     st = new StringTokenizer(s, ",");
     t = st.nextToken();
     if (t.equalsIgnoreCase("Courier"))
       fontName = "Courier";
     else if (t.equalsIgnoreCase("Dialog"))
       fontName = "Dialog";
     else if (t.equalsIgnoreCase("Helvetica"))
       fontName = "Helvetica";
     else if (t.equalsIgnoreCase("Symbol"))
       fontName = "Symbol";
     else if (t.equalsIgnoreCase("TimesRoman"))
       fontName = "TimesRoman";

     // Font style.

     t = st.nextToken();
     if (t.equalsIgnoreCase("plain"))
       fontStyle = Font.PLAIN;
     else if (t.equalsIgnoreCase("bold"))
       fontStyle = Font.BOLD;
     else if (t.equalsIgnoreCase("italic"))
       fontStyle = Font.ITALIC;
     else if (t.equalsIgnoreCase("boldItalic"))
       fontStyle = Font.BOLD + Font.ITALIC;

     // Font size.

     t = st.nextToken();
     if ((n = Integer.parseInt(t)) > 0)
         fontSize = n;
   }
   catch (Exception e) {}

   // Get the pop-up text box margins.

   try {
     s = getParameter("margins");
     if (s != null) {
       st = new StringTokenizer(s, ",");
       if ((n = Integer.parseInt(st.nextToken())) > 0)
         hrznMargin = n;
       if ((n = Integer.parseInt(st.nextToken())) > 0)
         vertMargin = n;
     }
   }
   catch (Exception e) {}

   // Get the area outline color.

   try {
     s = getParameter("outline");
     if (s != null) {
       olColor = getColorParm(s);
       olFlag = true;
     }
   }
   catch (Exception e) {}

   // Get the default URL, target and status window message for the image map.

   try {
     s = getParameter("default");
     st = new StringTokenizer(s, ",");
     t = st.nextToken();
     try {
       url = new URL(getDocumentBase(), t);
       if (st.hasMoreTokens()) {
         t = st.nextToken();
         target = t;
       }
       defaultUrl = url;
     }
     catch (MalformedURLException e) {}
   }
   catch (Exception e) {}

   try {
     s = getParameter("status");
     if (s != null)
       status = s;
   }
   catch (Exception e) {}

   // Get data for each image map area.

   s = null;
   i = 1;
   do {
     try {
       s = getParameter("shape-" + i);
       if (s != null) {
         getShape(s);

         // Get the URL, target and status window message.

         try {
           t = getParameter("url-" + i);
           st = new StringTokenizer(t, ",");
           u = st.nextToken();
           try {
             url = new URL(getDocumentBase(), u);
             if (st.hasMoreTokens()) {
               u = st.nextToken();
               area.setURL(url, u);
             }
             else
               area.setURL(url);
           }
           catch (MalformedURLException e) {}
         }
         catch (Exception e) {}

         try {
           t = getParameter("status-" + i);
           if (t != null)
             area.setStatusMsg(t);
           }
         catch (Exception e) {}

         // Get text to display in the pop-up text box for this area.

         t = null;
         j = 1;
         do {
           try {
             t = getParameter("text-" + i + "-" + j);
             if (t != null) {
               st = new StringTokenizer(t, "|");
               if (st.countTokens() > 1) {
                 n = Integer.parseInt(st.nextToken());
                 u = st.nextToken();
               }
               else {
                 n = 0;
                 u = t;
               }
               area.addText(n, u);
             }
           }
           catch (Exception e) {}
           j++;
         } while (t != null);

         // Add the area to the list.

         areas.addElement(area);
       }
     }
     catch (Exception e) {}
     i++;
   } while (s != null);

   // Trim the areas list to reclaim unused space.

   areas.trimToSize();

   // Set size values based on the font.

   g = getGraphics();
   font = g.getFont();
   g.setFont(font = new Font(fontName, fontStyle, fontSize));
   fm = g.getFontMetrics();
   xAdvance = fm.getMaxAdvance();
   yAscent = fm.getMaxAscent();
   yHeight = fm.getHeight();

   // Calculate the size of the pop-up text box for each area.

   for (i = 0; i < areas.size(); i++) {
     area = (IMapArea) areas.elementAt(i);
     for (j = 0; j < area.getCount(); j++) {
       s = area.getText(j);
       n = area.getIndent(j) * xAdvance + fm.stringWidth(s);
       if (n > area.width)
         area.width  = n;
     }
     area.width += 2 * hrznMargin;
     area.height = area.getCount() * yHeight + 2 * vertMargin;
   }
 }

 private Color getColorParm(String s) {

     int r, g, b;

     // Check if a pre-defined color is specified.

     if (s.equalsIgnoreCase("black"))
       return(Color.black);
     if (s.equalsIgnoreCase("blue"))
       return(Color.blue);
     if (s.equalsIgnoreCase("cyan"))
       return(Color.cyan);
     if (s.equalsIgnoreCase("darkGray"))
       return(Color.darkGray);
     if (s.equalsIgnoreCase("gray"))
       return(Color.gray);
     if (s.equalsIgnoreCase("green"))
       return(Color.green);
     if (s.equalsIgnoreCase("lightGray"))
       return(Color.lightGray);
     if (s.equalsIgnoreCase("magenta"))
       return(Color.magenta);
     if (s.equalsIgnoreCase("orange"))
       return(Color.orange);
     if (s.equalsIgnoreCase("pink"))
       return(Color.pink);
     if (s.equalsIgnoreCase("red"))
       return(Color.red);
     if (s.equalsIgnoreCase("white"))
       return(Color.white);
     if (s.equalsIgnoreCase("yellow"))
       return(Color.yellow);

     // If the color is specified in HTML format, build it from the red, green and blue values.

     if (s.length() == 7 && s.charAt(0) == '#') {
       r = Integer.parseInt(s.substring(1,3),16);
       g = Integer.parseInt(s.substring(3,5),16);
       b = Integer.parseInt(s.substring(5,7),16);
       return(new Color(r, g, b));
     }

     // If we can't figure it out, default to black.

     return(Color.black);
 }

 private void getShape(String s) {

   StringTokenizer st;
   String t;
   int x1, y1, x2, y2;
   int x, y, a, b;
   Polygon poly;

   // Set the area depending on the supplied shape parameters. (Note that a circle is a special
   // case of an ellipse where a = b.)

   st = new StringTokenizer(s, ",");
   t  = st.nextToken();
   if (t.equalsIgnoreCase("rect")) {
     x1 = Integer.parseInt(st.nextToken());
     y1 = Integer.parseInt(st.nextToken());
     x2 = Integer.parseInt(st.nextToken());
     y2 = Integer.parseInt(st.nextToken());
     area = new IMapArea(new Rectangle(x1, y1, x2 - x1, y2 - y1));
     return;
   }
   if (t.equalsIgnoreCase("circle")) {
     x = Integer.parseInt(st.nextToken());
     y = Integer.parseInt(st.nextToken());
     a = Integer.parseInt(st.nextToken());
     area = new IMapArea(x, y, a, a);
     return;
   }
   if (t.equalsIgnoreCase("ellipse")) {
     x = Integer.parseInt(st.nextToken());
     y = Integer.parseInt(st.nextToken());
     a = Integer.parseInt(st.nextToken());
     b = Integer.parseInt(st.nextToken());
     area = new IMapArea(x, y, a, b);
     return;
   }
   if (t.equalsIgnoreCase("poly")) {
     poly = new Polygon();
     while (st.hasMoreTokens()) {
       x = Integer.parseInt(st.nextToken());
       y = Integer.parseInt(st.nextToken());
       poly.addPoint(x, y);
     }
     area = new IMapArea(poly);
   }
 }

 public boolean mouseExit(Event e, int x, int y) {

   // Deactivate any currently active area and clear the status window.

   activeArea = -1;
   getAppletContext().showStatus("");
   repaint();
   return true;
 }

 public boolean mouseMove(Event e, int x, int y) {

   int last;
   int i;

   // Save the currently active area.

   last = activeArea;

   // If the mouse is over a area, mark it as active. (Go backwards thru the list to match the
   // behavior of image maps when areas overlap.)

   activeArea = -1;
   for (i = areas.size() - 1; i >= 0; i--) {
     area = (IMapArea) areas.elementAt(i);
     if (area.inside(x, y))
       activeArea = i;
   }

   // Update the display only if the active area has changed (this will save some processor
   // cycles).

   if (activeArea != last) {
     if (activeArea >= 0) {
       area = (IMapArea) areas.elementAt(activeArea);
       if (area.status != null)
         getAppletContext().showStatus(area.status);
       else if (area.url != null)
         getAppletContext().showStatus(area.url.toString());
       else
         getAppletContext().showStatus("");
     }

// here is our little experiment area

     area = (IMapArea) areas.elementAt(activeArea);
     if (area.url != null)
       if (area.target != null)
         getAppletContext().showDocument(area.url, area.target);
       else
         getAppletContext().showDocument(area.url);


     repaint();
   }

   // When no area is active, show the default status window message.

   if (activeArea < 0) {
     if (status != null)
       getAppletContext().showStatus(status);
     else if (defaultUrl != null)
       getAppletContext().showStatus(defaultUrl.toString());
     else
       getAppletContext().showStatus("");
   }

   return true;
 }

 public boolean mouseDown(Event e, int x, int y) {

   // If there is currently an active area with a URL, link to it.

   if (activeArea >= 0) {
     area = (IMapArea) areas.elementAt(activeArea);
     if (area.url != null)
       if (area.target != null)
         getAppletContext().showDocument(area.url, area.target);
       else
         getAppletContext().showDocument(area.url);
   }

   // Otherwise, if a defaut URL was specified, link to it.

   else if (defaultUrl != null) {
     if (target != null)
       getAppletContext().showDocument(defaultUrl, target);
     else
       getAppletContext().showDocument(defaultUrl);
   }

   // No URL to link to, just return.

   return true;
 }

 public void paint(Graphics g) {

   update(g);
 }

 public void update(Graphics g) {

   Dimension d = size();
   int x, y;
   int i, j;
   int n;
   String s;
   Rectangle rect;
   Polygon poly;

   // Create the offscreen graphics context, if no good one exists.

   if (offGraphics == null || d.width != offDimension.width || d.height != offDimension.height) {
     offDimension = d;
     offImage = createImage(d.width, d.height);
     offGraphics = offImage.getGraphics();
   }

   // If the image map has finished loading, fill the canvas with the background color and draw
   // the map image over it.

   if ((checkImage(mapImage, this) & ImageObserver.ALLBITS) == ImageObserver.ALLBITS) {
     offGraphics.setColor(pgColor);
     offGraphics.fillRect(0, 0, d.width, d.height);
     offGraphics.drawImage(mapImage, 0, 0, this);
   }

   // Otherwise, put up a message and return.

   else {
     offGraphics.setColor(bgColor);
     offGraphics.fillRect(0, 0, d.width, d.height);
     offGraphics.setFont(font);
     offGraphics.setColor(fgColor);
     offGraphics.drawString("Loading image...", xAdvance, yHeight + yAscent);
     g.drawImage(offImage, 0, 0, this);
     return;
   }

   // If there is a currently active area, process it.

   if (activeArea >= 0) {
     area = (IMapArea) areas.elementAt(activeArea);
     rect = area.getBoundingBox();

     // Draw the outline of the area if the outline flag is set.

     if (olFlag) {
       offGraphics.setColor(olColor);
       if (area.isRect()) {
         rect = area.getRect();
         offGraphics.drawRect(rect.x, rect.y, rect.width, rect.height);
       }
       else if (area.isEllipse()) {
         rect = area.getEllipse();
         offGraphics.drawOval(rect.x, rect.y, rect.width, rect.height);
       }
       else if (area.isPoly()) {
         poly = area.getPoly();
         offGraphics.drawPolygon(poly);
         n = poly.npoints - 1;
         offGraphics.drawLine(poly.xpoints[n], poly.ypoints[n], poly.xpoints[0], poly.ypoints[0]);
       }
     }

     // Draw the pop-up text box for the active area if there is any text.

     if (area.getCount() > 0) {

       // Determine a starting position for the text box. An attempt is made to keep it from
       // overlaying the area if possible.

       x = 0; y = 0;

       // Get the x-coord for the text box.

       if (d.width - (rect.x + rect.width) > rect.x)
         x = rect.x + rect.width + xAdvance;
       else
         x = rect.x - area.width - xAdvance;
       if (x + area.width > d.width)
         x = d.width - area.width;
       x = Math.max(0, x);

       // Get the y-coord for the box.

       if ((x >= rect.x && x <= rect.x + rect.width) ||
           (x + area.width >= rect.x && x + area.width <= rect.x + rect.width)) {
         if (d.height - (rect.y + rect.height) > rect.y)
           y = rect.y + rect.height + yHeight;
         else
           y = rect.y - area.height - yHeight;
       }
       else {
         if (d.height - (rect.y + rect.height) > rect.y)
           y = rect.y + rect.height / 2;
         else
           y = rect.y + rect.height / 2 - area.height;
       }
       if (y + area.height > d.height)
         y = d.height - area.height;
       y = Math.max(0, y);

       // Draw the pop-up box and border.

       offGraphics.setColor(bdColor);
       offGraphics.fillRect(x, y, area.width, area.height);
       offGraphics.setColor(bgColor);
       offGraphics.fillRect(x + bdSize, y + bdSize, area.width - 2 * bdSize, area.height - 2 * bdSize);

       // Add the text.

       offGraphics.setFont(font);
       offGraphics.setColor(fgColor);
       for (i = 0; i < area.getCount(); i++) {
         j = 0;
         s = area.getText(i);
         n = area.getIndent(i);
         offGraphics.drawString(s, x + hrznMargin + xAdvance * n, y + vertMargin + i * yHeight + yAscent);
       }
     }
   }

   // Paint the image onto the screen.

   g.drawImage(offImage, 0, 0, this);
 }
}