Chapter 23

Software Cost Estimator


CONTENTS

This chapter describes how to build an applet that enables you to forecast the amount of time needed to develop software. This chapter puts to use much of what you've learned throughout this book, especially on the subject of user interface programming. Additionally, you will learn some new tricks, such as how to create and use an imagemap.

The Project Overview

In this chapter you will build the CostEstimator applet. This applet is essentially a COCOMO calculator. COCOMO, which stands for Constructive Cost Model, is a method for estimating the amount of time completing a software development project will take. In COCOMO you assess a project based on 15 attributes, such as programmer capability, required reliability, and product complexity. Each of these attributes is rated on a scale from Very Low to Extra High.

The effort of a project, as measured in person months, is calculated in COCOMO with the following equation:

Effort = 3.0 * (KLOC) (1.12 * EAF)

KLOC represents a measure of the number of thousands of lines of code. For example, if you anticipate writing 10,500 lines of code, KLOC is 10.5. EAF, which stands for Effort Adjustment Factor, is calculated, as you'll see in a moment, based on the attributes of the project being estimated. The magic numbers 3.0 and 1.12 were calculated by Boehm using a form of regression analysis. These values are for what Boehm refers to as "semi-detached" projects.

As mentioned earlier, in COCOMO a project is rated from Very Low to Extra High, on 15 attributes. Because how a project is rated on these attributes influences the cost of the project, the attributes are known as cost drivers. The cost drivers and their associated values for each rating are shown in Table 23.1.

Table 23.1. COCOMO cost drivers.

Cost Drivers
Very Low
Low
Nominal
High
Very High
Extra High
Analyst capability
1.46
1.19
1.00
0.86
0.71
 
Applications
1.29
1.13
1.00
0.91
0.82
 
experience
 
 
 
 
 
 
Complexity
0.70
0.85
1.00
1.15
1.30
1.65
Database size
 
0.94
1.00
1.08
1.16
 
Execution time
 
 
1.00
1.11
1.30
1.66
Language experience
1.14
1.07
1.00
0.95
 
 
Modern practices
1.24
1.10
1.00
0.91
0.82
 
Programmer capability
1.42
1.17
1.00
0.86
0.70
 
Reliability
0.75
0.88
1.00
1.15
1.40
 
Schedule constraint
1.23
1.08
1.00
1.04
1.10
 
Software tools
1.24
1.10
1.00
0.91
0.83
 
Storage constraint
 
 
1.00
1.06
1.21
1.56
Turnaround time
 
0.87
1.00
1.07
1.15
 
Virtual Machine experience
1.21
1.10
1.00
0.90
 
 
Virtual Machine volatility
 
0.87
1.00
1.15
1.30
 

The Effort Adjustment Factor used in the Effort equation is calculated by taking the product of all 15 cost drivers. You should notice from Table 23.1 that the Nominal value for each cost driver is 1.00. This means you can think of Nominal as the default value for each cost driver. As an example of calculating the Effort Adjustment Factor, assume that a project is rated Nominal for everything except Programmer Capability (which is High) and Complexity (which is Very High). This means that the Effort Adjustment Factor for this project is as follows:

EAF = 1.00 13 * 0.86 * 1.30

Multiplying this out, you get EAF = 1.118. Assume that you are developing an estimate for a project you expect will include 10,000 lines of code. These values can then be plugged into the Effort equation as shown here:

Effort = 3.0 * (10) (1.12 * 1.118)

As you'll be able to verify with the applet at the end of the chapter, this results in Effort equaling 53 person months.

In addition to the Effort equation, COCOMO provides an equation for calculating the duration in calendar months of a project. The equation for duration is this:

Duration = 2.5 * (Effort 0.35)

The CostEstimator applet developed in this chapter calculates both Effort and Duration.

NOTE
For a full description of COCOMO, see the book Software Engineering Economics, by Barry Boehm. This book, first published in 1981, remains valuable to this day and is one of the classics in our industry. If you have ever missed a deadline, been asked to give a deadline, met a deadline, or bet on a horse named Deadline, you should own a copy of Boehm's book.

The CostEstimator Applet

The rest of this chapter describes how to program the CostEstimator applet. The initial screen of this applet is shown in Figure 23.1. At the top left of the applet is an imagemap that includes the abbreviations PRD, CMP, PER, and PRJ. These stand for Product, Computer, Personnel, and Project. When you click the mouse over these areas on the imagemap, the applet displays a different set of cost drivers to the right of the imagemap. In Figure 23.1, the Product cost drivers are displayed because PRD has been selected in the imagemap, as shown by the highlighting around it.

Figure 23.1 : The initial screen of CostEstimator while Product cost drivers are being selected.

Selecting a different portion of the imagemap displays a different set of fields related to different cost drivers. For example, Figure 23.2 shows the Project attributes. In this case, two choice fields and a checkbox group are used. The four categories of cost drivers are shown in Table 23.2.

Figure 23.2 : Selecting Project cost drivers in the CostEstimator.


Table 23.2. Cost drivers by category.

CategoryCost Driver
ProductReliability
 Database size
 Complexity
ComputerExecution time
 Storage constraints
 VM volatility
 Turnaround time
PersonnelAnalyst capability
 Applications experience
 Programmer capability
 VM experience
 Language experience
ProjectModern practices
 Software tools
 Schedule constraints

Figures 23.1 and 23.2 also show an area for entry of KLOC. As you recall, KLOC is shorthand for Thousand lines of code. Non-editable text fields are also provided. They will display the Effort Adjustment Factor and the COCOMO-estimated number of person months and calendar months. In these figures, numbers are shown for the example described earlier in this chapter. This is a 10,000-line program that will take 53 person months to complete.

Three buttons are also provided. The Summary button displays a frame detailing the cost-driver selections that have been made. The Recalc button performs the COCOMO calculations and updates the non-editable fields. The Reset button restores all the cost-driver selections to Nominal. This is useful for analyzing different scenarios.

The Class Overview

The classes are used to write the CostEstimator applet. Classes whose names are italicized are specific to this applet. Classes without italicized names are standard Java classes that are used as base classes for CostEstimator classes.

The CostEstimator class is, of course, the main class and extends Applet. The CostDriverPanel class is an abstract class that is implemented by ChoicePanel and ProjectPanel. These classes control the area at the top right of the applet where values for the cost drivers are entered. The fields for each of the cost-driver categories are collected into one of the subclasses of CostDriverPanel.

The Rating class is a low-level class that associates a rating (for example, "Low") with a value (1.19) within a cost driver ("Analyst capability"). The CostDriver class stores all information about the cost driver, including its name and possible ratings.

The ImagemapRect and ImageCanvas classes are used to create the imagemap. An imagemap is painted on the ImageCanvas. An ImagemapRect indicates an area on the ImageCanvas that causes the displayed CostDriverPanel to change.

Finally, the SummaryFrame class is used to provide a frame that will appear in response to pressing the Summary button. The SummaryFrame displays details on the values selected for each cost driver.

Storing Cost Drivers and Ratings

An instance of the CostDriver class, shown in Listing 23.1, is used to represent one of the 15 COCOMO cost drivers. The constructor for CostDriver is passed the name of the driver and a vector of ratings. This vector contains instances of the Rating class, which is shown in Listing 23.2.


Listing 23.1. CostDriver.java.
import java.util.*;
import Rating;
// This class stores the name of a cost driver (e.g.,
// "Reliability") and the ratings (e.g., "Low", "Nominal",
// "High") that can be selected for it.
class CostDriver extends Object
{
  public String name;    // the name of the driver, for
               // example, "Reliability"
  public Vector ratings;
  public CostDriver(String name, Vector ratings)
  {
    this.name = name;
    this.ratings = (Vector)ratings.clone();
  }
}


Listing 23.2. Rating.java.
// This class represents a rating that can be selected for a
// cost driver. It stores both the name of the rating (Low, High,
// etc.) and the multiplier to use if this rating is selected.
// For example, the High rating for Complexity implies a
// multiplier of 1.15.
public class Rating extends Object
{
  public String name;
  public float multiplier;
  Rating(String str, float value)
  {
    name = str;
    multiplier = value;
  }
}

The Rating class represents a paired name and multiplier. For example, the Very Low rating for Reliability implies a multiplier of 0.75. A new Rating object could be created with these values as shown here:

new Rating("Very low", 0.75F);

To create a new instance of the CostDriver class, you create a vector of Rating objects and pass this to the CostDriver constructor. This could be carried out in the following way:

// create a vector of Reliability ratings
Vector relyRatings = new Vector(5);
relyRatings.addElement(new Rating("Very low",  0.75F));
relyRatings.addElement(new Rating("Low",    0.88F));
relyRatings.addElement(new Rating("Nominal",  1.00F));
relyRatings.addElement(new Rating("High",    1.15F));
relyRatings.addElement(new Rating("Very high", 1.40F));
// create the new cost driver
CostDriver cd = new CostDriver("Reliability", relyRatings);

Programming the User Interface

To create the user interface for the CostEstimator, you need to use the GridBagLayout layout manager. The other layout managers do not offer the flexibility necessary to create this user interface. When you consider the main screen of the CostEstimator, you realize that it is readily partitioned into four main areas: an area for the imagemap, an area for entering values for the cost drivers, an area for entering KLOC and viewing the results, and an area containing the buttons. Figure 23.3 shows the main screen of the CostEstimator but has been annotated with lines that indicate the boundaries of these areas.

Figure 23.3 : The four main areas of the CostEstimator user interface.

To create this interface, the init method of CostEstimator creates a new GridBagLayout layout manager and assigns it to the applet. The init method then calls four worker methods, each of which creates one of the areas shown in Figure 23.4. This can be seen in the init method, as shown here:

Figure 23.4 : A ChoicePanel displaying personnel-related cost drivers.

public void init()
{
  // Create a GridBagLayout and use it as the applet's
  // layout manager
  GridBagLayout layout = new GridBagLayout();
  setLayout(layout);
  // create each of the main areas of the user interface
  CreateImagemap(layout);
  CreateCardPanel(layout);
  CreateResultsPanel(layout);
  CreateButtonPanel(layout);
  resize(400, 320);
}

Creating the Imagemap

The first of the four areas on the applet user interface is an imagemap. An imagemap is a bitmap that includes selectable areas within it. In this case the imagemap is a bitmap showing abbreviations for each of four groupings of COCOMO cost drivers. When the user clicks the mouse over the PRD area of the imagemap, the product-related cost drivers are displayed to the right of the imagemap. Similarly, clicking the mouse over CMP displays the computer-related cost drivers, PER displays personnel-related cost drivers, and PRJ displays project-related cost drivers.

The method CreateImagemap is called from within init and is as shown here:

private void CreateImagemap(GridBagLayout parentLayout)
{
  // create the image
  Image image = getImage(getDocumentBase(), "map.gif");
  // create a canvas on which the image will be displayed
  imageCanvas = new ImageCanvas(image);
  // place the canvas on the applet using the applet's
  // layout manager
  GridBagConstraints gbc = new GridBagConstraints();
  gbc.gridwidth = GridBagConstraints.RELATIVE;
  gbc.fill = GridBagConstraints.BOTH;
  parentLayout.setConstraints(imageCanvas, gbc);
  add(imageCanvas);
}

This method loads the image map.gif from the applet's base directory. It then creates an image canvas based on the retrieved image. The image canvas is then placed on the applet using the parentLayout parameter. The ImageCanvas class encapsulates the details of the imagemap. ImageCanvas is shown in Listing 23.3.


Listing 23.3. The ImageCanvas class.
public class ImageCanvas extends Canvas
{
  private Image image;
  private Vector rects;
  private ImagemapRect lastrect;
  // construct an image canvas based on the specified image
  // and the currently selected portion of the imagemap
  ImageCanvas(Image image)
  {
    resize(50,50);
    this.image=image;
    // allocate a vector to hold the ImagemapRects
    rects = new Vector();
    // all the ImagemapRects that describe the hotspots
    // on the imagemap
    rects.addElement(new ImagemapRect(0,  0, 50, 50,
        "Product"));
    rects.addElement(new ImagemapRect(49, 0, 50, 50,
        "Computer"));
    rects.addElement(new ImagemapRect(0, 49, 50, 50,
        "Personnel"));
    rects.addElement(new ImagemapRect(49, 49, 50, 50,
        "Project"));
    // store the last selected ImagemapRect
    lastrect = (ImagemapRect)rects.elementAt(0);
  }
  public void paint(Graphics g)
  {
    // paint the image
    g.drawImage(image, 0, 0, this);
    // highlight the selected location
    g.setXORMode(Color.red);
    g.drawRect(lastrect.x, lastrect.y, lastrect.width,
        lastrect.height);
    g.drawRect(lastrect.x+1, lastrect.y+1, lastrect.width-2,
        lastrect.height-2);
    g.drawRect(lastrect.x+2, lastrect.y+2, lastrect.width-4,
        lastrect.height-4);
  }
  // Determine if the mouse pointer was clicked over an area in
  // the imagemap that isn't already the current selection
  public boolean NewImageSelected(int x, int y,
      StringBuffer cardName)
  {
    boolean result = false;    // assume failure
    // normalize the coordinates relative to the ImageCanvas
    int xPos = x - bounds().x;
    int yPos = y - bounds().y;
    // determine which rectangle (if any) was selected
    ImagemapRect r = InWhichRect(xPos, yPos);
    // if a rectangle was selected and it isn't the same
    // one that is already selected, a new area has been
    // selected
    if (r != null && r != lastrect)
    {
      lastrect = r;
      cardName.append(r.cardName);
      result = true;
    }
    return result;
  }
  // determine which ImageMapRect (if any) the mouse pointer
  // is in
  private ImagemapRect InWhichRect(int x, int y)
  {
    // use an enumeration to move through each ImagemapRect
    // until finding one which encloses the point (x, y)
    for (Enumeration enum = rects.elements();
        enum.hasMoreElements(); )
    {
      ImagemapRect r = (ImagemapRect)enum.nextElement();
      if (r.inside(x, y))
        return r;
    }
    return null;
  }
}

ImageCanvas extends Canvas by drawing an image on the canvas and by storing a vector of areas that represent the selectable areas of the imagemap. The ImageCanvas constructor is passed the image that will be displayed and stores a reference to it. It then allocates a vector named rects and uses addElement to add four new instances of the ImageRect class to the vector.

The ImageRect class stores the location and dimensions of a rectangle and a name representing that area. For example, ImagemapRect(0, 49, 50, 50, "Personnel") creates an ImageRect named Personnel that begins at 0, 49 with both width and height of 50. The ImageRect class is written as follows:

class ImagemapRect extends Rectangle {
  String cardName;
  public ImagemapRect(int x, int y,int w,int h, String cardName)
  {
    super(x,y,w,h);
    this.cardName = cardName;
  }
}

The paint method for ImageCanvas uses drawImage to display the image. To indicate which area in the imagemap is currently selected, a red border is drawn around the most recently selected ImageRect. The member variable lastrect always holds the most recently selected ImageRect, so setXORMode and three calls to drawRect are used to draw the red border.

The methods NewImageSelected and InWhichRect are used to determine whether the user has clicked the mouse over an ImageRect within the imagemap. NewImageSelected is passed the x and y coordinates of the mouse click. It then normalizes these relative to the coordinates of the canvas using bounds. NewImageSelected then calls InWhichRect to determine in which ImagemapRect, if any, the mouse click occurred. If the mouse was clicked inside an ImagemapRect that is not the same as lastrect, the new value is stored in lastrect and the name of the selection is stored.

In the CostEstimator applet class it is necessary to override the mouseDown method to look for mouse button presses that occur when the mouse pointer is over the imagemap. This is done as shown here:

public boolean mouseDown(Event e, int x, int y)
{
  StringBuffer cardName = new StringBuffer();
  // if the button was pressed when the mouse pointer was
  // over the imagemap, get the name of the card to display
  // in the cardPanel.
  if (imageCanvas.NewImageSelected(x,y,cardName) == true) {
    cardLayout.show(cardPanel, cardName.toString());
    imageCanvas.repaint();
  }
  return true;
}

Selecting Project Attributes

The second area on the applet user interface is for entering values for the various cost drivers. The group of fields displayed in this area is controlled by the user's selection in the imagemap. Because this area is used to display different fields at different times, a panel is placed over this entire area, and a CardLayout is assigned as the panel's layout manager. Depending on the imagemap selection, the appropriate card is displayed on the panel. This can be seen in the following code:

private void CreateCardPanel(GridBagLayout parentLayout)
{
  // create the panel
  cardPanel = new Panel();
  // place the panel on the applet using the applet's
  // layout manager
  GridBagConstraints gbc = new GridBagConstraints();
  gbc.gridwidth = GridBagConstraints.REMAINDER;
  gbc.fill = GridBagConstraints.BOTH;
  gbc.weightx=1;
  parentLayout.setConstraints(cardPanel, gbc);
  add(cardPanel);
  // create a CardLayout and assign it to the panel
  cardLayout = new CardLayout();
  cardPanel.setLayout(cardLayout);
  // create a vector to hold the CostDriverPanels that
  // will be placed on cardPanel
  CostDriverPanelVector = new Vector(4);
  // create each of the panels that can be displayed
  // on this card
  CreateProductPanel();
  CreateComputerPanel();
  CreatePersonnelPanel();
  CreateProjectPanel();
}

The CreateCardPanel method allocates a new Panel instance named cardPanel. It then creates an instance of GridBagConstraints and establishes constraints that the panel is the last item on the line and that it should grow in both directions. The panel is added to the applet using parentLayout, the GridBagLayout layout manager that was allocated in init to handle the applet's overall layout.

Next, a new CardLayout is constructed and assigned to the panel. A four-element vector is then allocated. This vector will be used to store each of the four panels that will be placed on the CardPanel and that will be toggled using the imagemap. Finally, each of the four methods at the end of CreateCardPanel creates one of the panels displayed on CardPanel.

The CostDriverPanel Class

Each of the panels that will be placed on CardPanel will be of type CostDriverPanel. CostDriverPanel, however, is an abstract class. Two subclasses of CostDriverPanel, ChoicePanel and ProjectPanel, are provided and are concrete classes that can be constructed.

The CostDriverPanel class, as shown in Listing 23.4, provides an overridden paint method that draws a border around the panel. Additionally, three abstract methods (getMultiplier, SetDefaults, and GetSummary) are declared. The getMultiplier method is used to get the partial Effort Adjustment Factor for the cost drivers on the panel. The SetDefaults method is used to restore each of the cost drivers on the panel to its default value. GetSummary is used to generate a string summarizing the selections for the cost drivers on the panel.


Listing 23.4. CostDriverPanel.java.
import java.applet.*;
import java.awt.*;
import java.util.*;
// This is an abstract base class that can be extended
// for displaying cost drivers
public abstract class CostDriverPanel extends Panel
{
  public CostDriverPanel()
  {
    super();
  }
  public void paint(Graphics g)
  {
    // draw a rectangle around the panel
    Rectangle rect = bounds();
    g.drawRect(rect.x, rect.y, rect.x+rect.width-1,
        rect.y + rect.height-1);
  }
  // calculate the effort adjustment multiplier for
  // the cost drivers on this panel
  public abstract float getMultiplier();
  // set the default values for all cost drivers on this panel
  public abstract void SetDefaults();
  // return a String summarizing selections on this panel
  public abstract String GetSummary();
}

The ChoicePanel Class

The ChoicePanel class is a non-abstract subclass of CostDriverPanel. A ChoicePanel can be used to automatically position and display cost drivers. Each cost driver will be displayed as a choice field. Figure 23.4 shows the ChoicePanel created using the personnel cost drivers.

Each ChoicePanel object contains the following member variables:

Choice choiceArray[];
Vector costDrivers;

The choiceArray is an array of choice variables. One item is allocated in the array for each of the cost drivers displayed on the ChoicePanel. The costDrivers vector holds instances of the CostDriver class that was described earlier in this chapter. These variables are both used extensively in the ChoicePanel constructor, which is shown in Listing 23.5.

The constructor is passed a vector of CostDriver objects, and these are cloned into the costDrivers member variable. The choiceArray is allocated based on the size of this vector. Next, the layout manager for the ChoicePanel is constructed and assigned to the object. Because we want to place a Label and Choice objects on the same lines, a GridBagLayout is used.


Listing 23.5. The ChoicePanel constructor.
public ChoicePanel(Vector drivers)
{
  super();    // construct the parent class
  // clone the drivers
  costDrivers = (Vector) drivers.clone();
  // allocate an array of Choice fields, each cost driver
  // will be entered in a Choice field
  choiceArray = new Choice[costDrivers.size()];
  // create and use a GridBagLayout layout manager
  GridBagLayout layout = new GridBagLayout();
  GridBagConstraints gbc = new GridBagConstraints();
  setLayout(layout);
  // use an enumeration to move through the cost drivers to
  // be displayed on this panel
  int i = 0;
  for (Enumeration e=costDrivers.elements();
      e.hasMoreElements(); i++)
  {
    CostDriver cd = (CostDriver)e.nextElement();
    // allocate a Choice for each cost driver
    choiceArray[i] = new Choice();
    // use an enumeration to move through the ratings
    // within each cost driver (e.g., Low, Nominal, High)
    for (Enumeration enum = cd.ratings.elements();
        enum.hasMoreElements() ;)
    {
      Rating r = (Rating)enum.nextElement();
      // add the name of the rating to the Choice
      choiceArray[i].addItem(r.name);
    }
    // select this item by default
    choiceArray[i].select("Nominal");
    // add the label to the panel
    gbc.gridwidth = 1;
    Label label = new Label(cd.name);
    gbc.anchor = GridBagConstraints.EAST;
    layout.setConstraints(label, gbc);
    add(label);
    // add the Choice field to the panel
    gbc.gridwidth = GridBagConstraints.REMAINDER;
    gbc.anchor = GridBagConstraints.WEST;
    gbc.insets.left=5;
    layout.setConstraints(choiceArray[i], gbc);
    add(choiceArray[i]);
  }
}

Next, an enumeration is used to move through the elements in the costDrivers vector. For each element, the CostDriver is cast into a local variable, cd, and an item in the choiceArray is allocated to hold the choice object that will contain the selections for this cost driver. After the elements are added to the choice, select("Nominal") is used to set the default item. Finally, a label and the choice are added to the panel.

Within the CostEstimator class, ChoicePanel objects are constructed for the product, computer, and personnel cost-estimator categories. The project category uses a different subclass of CostDriverPanel to display its cost drivers. These choice panels are created by the methods CreateProductPanel, CreateComputerPanel, and CreatePersonnelPanel. These methods are very similar. The CreatePersonnelPanel method is shown in Listing 23.6.


Listing 23.6. CreatePersonnelPanel in the CostEstimator class.
private void CreatePersonnelPanel()
{
  // allocate a vector that will hold the cost drivers
  // that are displayed on this panel
  Vector costDrivers = new Vector(5);
  // create a vector of Analyst Capability ratings
  Vector acapRatings = new Vector(5);
  acapRatings.addElement(new Rating("Very low",  1.46F));
  acapRatings.addElement(new Rating("Low",    1.19F));
  acapRatings.addElement(new Rating("Nominal",  1.00F));
  acapRatings.addElement(new Rating("High",    0.86F));
  acapRatings.addElement(new Rating("Very high", 0.71F));
  // create the Analyst Capability CostDriver and add it to
  // the vector
  costDrivers.addElement(new CostDriver("Analyst Capability",
      acapRatings));
  // create a vector of Applications Experience ratings
  Vector aexpRatings = new Vector(5);
  aexpRatings.addElement(new Rating("Very low",  1.29F));
  aexpRatings.addElement(new Rating("Low",    1.13F));
  aexpRatings.addElement(new Rating("Nominal",  1.00F));
  aexpRatings.addElement(new Rating("High",    0.91F));
  aexpRatings.addElement(new Rating("Very high", 0.82F));
  // create the Applications Experience CostDriver and add
  // it to the vector
  costDrivers.addElement(new CostDriver(
      "Applications Experience", aexpRatings));
  // create a vector of Programmer Capability ratings
  Vector pcapRatings = new Vector(5);
  pcapRatings.addElement(new Rating("Very low",  1.42F));
  pcapRatings.addElement(new Rating("Low",    1.17F));
  pcapRatings.addElement(new Rating("Nominal",  1.00F));
  pcapRatings.addElement(new Rating("High",    0.86F));
  pcapRatings.addElement(new Rating("Very high", 0.70F));
  // create the Programmer Capability CostDriver and add
  // it to the vector
  costDrivers.addElement(new CostDriver(
      "Programmer Capability", pcapRatings));
  // create a vector of Virtual Machine Experience ratings
  Vector vexpRatings = new Vector(4);
  vexpRatings.addElement(new Rating("Very low",  1.21F));
  vexpRatings.addElement(new Rating("Low",    1.10F));
  vexpRatings.addElement(new Rating("Nominal",  1.00F));
  vexpRatings.addElement(new Rating("High",    0.90F));
  // create the Virtual Machine Experience CostDriver and
  // add it to the vector
  costDrivers.addElement(new CostDriver(
      "Virtual Machine Experience", vexpRatings));
  // create a vector of Language Experience ratings
  Vector lexpRatings = new Vector(4);
  lexpRatings.addElement(new Rating("Very low",  1.14F));
  lexpRatings.addElement(new Rating("Low",    1.07F));
  lexpRatings.addElement(new Rating("Nominal",  1.00F));
  lexpRatings.addElement(new Rating("High",    0.95F));
  // create the Language Experience CostDriver and add
  // it to the vector
  costDrivers.addElement(new CostDriver(
      "Language Experience", lexpRatings));
  // create the new panel and add it to the card
  ChoicePanel personnelPanel = new ChoicePanel(costDrivers);
  cardPanel.add("Personnel", personnelPanel);

  // add this panel to the vector of all CostDriverPanels
  CostDriverPanelVector.addElement(personnelPanel);
}

CreatePersonnelPanel allocates a vector, costDrivers, that is used to hold the cost drivers that will be displayed when the personnel area of the imagemap has been selected. For each individual cost driver, a vector is allocated and addElement is used to add new Rating objects. As you saw earlier in this chapter, a Rating object is constructed with the name and value for the rating. For example, a Very Low rating for Language Experience is associated with an Effort Adjustment Factor of 1.14. After each of the vectors is created and added to costDrivers, a new choice panel is created and the new choice panel is added to the vector of all ChoicePanels.

Because ChoicePanel is a subclass of CostDriverPanel, it must provide implementations of the getMultiplier, SetDefaults, and GetSummary methods. The getMultiplier method of ChoicePanel is implemented as follows:

public float getMultiplier()
{
  float value = 1.0f;
  int i = 0;
  // Use an enumeration to move through each of the cost
  // drivers on this panel.
  for (Enumeration e=costDrivers.elements();
      e.hasMoreElements(); i++)
  {
    // For each cost driver, get the rating associated
    // with the current selection in the Choice field.
    CostDriver cd = (CostDriver)e.nextElement();
    int index = choiceArray[i].getSelectedIndex();
    Rating r = (Rating)cd.ratings.elementAt(index);
    // multiply each of the multipliers together to
    // get the final result.
    value *= r.multiplier;
  }
  return value;
}

To calculate the multiplier, a local variable, value, is declared and set to 1.0 initially. An enumeration then steps through each of the cost drivers on the choice panel. For each cost driver, getSelectedIndex selects the item number of the selection for each cost driver. The rating associated with each item is then retrieved from cd.ratings.elementAt(index). As the enumeration proceeds through the cost drivers, the value is updated by multiplying it with the multiplier member in the Rating object, r.

The SetDefaults method is much simpler. All that is necessary is to loop through the items in the array of Choice objects using select to ensure that the item labeled Nominal is selected for each choice. This is done as shown here:

public void SetDefaults()
{
  int qty = costDrivers.size();
  for (int i=0; i < qty; i++)
    choiceArray[i].select("Nominal");
}

The GetSummary method is used to generate a string that summarizes the selections made on a choice panel. For example, a string summarizing the personnel category of cost drivers could appear as shown here:

Analyst Capability
  Nominal
  1
Applications Experience
  Nominal
  1
Programmer Capability
  Nominal
  1
Virtual Machine Experience
  Nominal
  1
Language Experience
  High
  0.95

The GetSummary method of ChoicePanel is written like this:

public String GetSummary()
{
  StringBuffer buf = new StringBuffer();
  // use an enumeration to move through each of the drivers
  int i=0;
  for (Enumeration e=costDrivers.elements();
      e.hasMoreElements(); i++)
  {
    // append the name of the driver
    CostDriver cd = (CostDriver)e.nextElement();
    buf.append(cd.name+"\r\n");
    // append the name of the selected value
    buf.append("\t" + choiceArray[i].getSelectedItem() +
        "\r\n");
    // retrieve and then append the multiplier for the
    // selected value for this cost driver
    int index = choiceArray[i].getSelectedIndex();
    Rating r = (Rating)cd.ratings.elementAt(index);

    buf.append("\t" + r.multiplier + "\r\n");
  }
  return buf.toString();
}

As with many other ChoicePanel methods, an enumeration is used to move through the costDrivers vector. Because this method will be dynamically building a string of unknown length, a StringBuffer, rather than a String, is used to hold the text under construction. First, the name of the cost driver is added to the string buffer. Next, getSelectedItem is used to get the name of the selection for the Choice object. Finally, getSelectedIndex is used to get the index number of the selected item, and this value is passed to cd.ratings.elementAt to retrieve a Rating object. From this object, the Effort Adjustment Factor is read from r.multiplier and appended to the buffer.

The ProjectPanel Class

The ProjectPanel class, like ChoicePanel, is a subclass of CostDriverPanel. The ProjectPanel class is used only for displaying the project-related cost drivers. It is similar to ChoicePanel in that it also uses Choice objects to solicit user input. Choice objects, however, are used only for the Modern Practices and Software Tools cost drivers. Options for the Schedule Constraints cost driver are shown as a checkbox group, as shown in Figure 23.5.

Figure 23.5 : The ProjectPanel display-ing the project-related cost drivers.

Because the ProjectPanel uses both Choice and Checkbox objects, the following member variables are defined in ProjectPanel:

Choice choiceArray[];
Vector costDrivers;
Vector cbVector;
CheckboxGroup cbGroup;

The choiceArray and costDrivers members are used in the same way each was used in ChoicePanel. The cbVector member holds the individual checkbox objects that are placed on the panel. The variable cbGroup is a CheckboxGroup that consists of all the checkboxes on the panel.

The ProjectPanel constructor is shown in Listing 23.7. After calling super to create the panel, the constructor calls the method LoadCostDrivers. This method, shown in Listing 23.8, is similar to the method CreatePersonnelPanel that was shown in Listing 23.6. All that LoadCostDrivers does is create the costDrivers vector that will be used throughout ProjectPanel's other methods.


Listing 23.7. The ProjectPanel constructor.
public ProjectPanel()
{
  super();
  LoadCostDrivers();     // load values into costDrivers
  // allocate an array of Choice fields that will hold
  // all but one cost driver
  int qty = costDrivers.size();
  choiceArray = new Choice[qty - 1];
  // create and use a GridBagLayout layout manager
  GridBagLayout layout = new GridBagLayout();
  GridBagConstraints gbc = new GridBagConstraints();
  setLayout(layout);
  // Loop through the cost drivers, creating a Choice field
  // for each. Process all but the last one. The last one
  // will be displayed using Checkboxes instead of a Choice
  // field.
  int i = 0;
  for (i=0; i<qty-1; i++)
  {
    CostDriver cd = (CostDriver)costDrivers.elementAt(i);
    // allocate a Choice for each cost driver
    choiceArray[i] = new Choice();
    // use an enumeration to move through the ratings
    // within each cost driver (e.g., Low, Nominal, High)
    for (Enumeration enum = cd.ratings.elements();
        enum.hasMoreElements() ;)
    {
      Rating r = (Rating)enum.nextElement();
      // add the name of the rating to the Choice
      choiceArray[i].addItem(r.name);
    }
    // select this item by default
    choiceArray[i].select("Nominal");
    // add the label to the panel
    gbc.gridwidth = 1;
    Label label = new Label(cd.name);
    gbc.anchor = GridBagConstraints.EAST;
    layout.setConstraints(label, gbc);
    add(label);
    // add the Choice field to the panel
    gbc.gridwidth = GridBagConstraints.REMAINDER;
    gbc.anchor = GridBagConstraints.WEST;
    gbc.insets.left=5;
    layout.setConstraints(choiceArray[i], gbc);
    add(choiceArray[i]);
  }
  // use the last cost driver to make a CheckboxGroup
  CostDriver cd = (CostDriver)costDrivers.elementAt(qty-1);
  // create a vector to hold the Checkboxes
  cbVector = new Vector(6);
  // add the label for the Checkbox group
  gbc.gridwidth = 1;
  Label label = new Label(cd.name);
  gbc.gridwidth = GridBagConstraints.REMAINDER;
  gbc.anchor = GridBagConstraints.WEST;
  layout.setConstraints(label, gbc);
  add(label);
  // keep track of how many items are on each line, so that
  // items can be properly positioned.
  int itemsOnLine=0;
  // create a new CheckboxGroup
  cbGroup = new CheckboxGroup();
  // all the checkbox items will be anchored on the west
  // side, so set this value before the loop
  gbc.anchor = GridBagConstraints.WEST;
  // use an enumeration to move through the ratings for the
  // cost driver
  for (Enumeration enum = cd.ratings.elements();
      enum.hasMoreElements(); itemsOnLine++)
  {
    Rating r = (Rating)enum.nextElement();
    // the "Nominal" item should be checked by default
    boolean checked = r.name.equals("Nominal");
    // if line is almost full of items, tell this item to
    // use the remainder of the space
    if (itemsOnLine == 2)
    {
      gbc.gridwidth = GridBagConstraints.REMAINDER;
      itemsOnLine = 0;
    }
    else
      gbc.gridwidth = 1;
    // create the new checkbox and add it to the screen
    Checkbox cb = new Checkbox(r.name, cbGroup, checked);
    layout.setConstraints(cb, gbc);
    add(cb);
    // then add it to the vector of checkboxes
    cbVector.addElement(cb);
  }
}


Listing 23.8. The ProjectPanel.LoadCostDrivers method.
private void LoadCostDrivers()
{
  // allocate a vector that will hold the cost drivers
  // that are displayed on this panel
  costDrivers = new Vector(3);
  // create a vector of Modern Practices ratings
  Vector modpRatings = new Vector(5);
  modpRatings.addElement(new Rating("Very low",  1.24F));
  modpRatings.addElement(new Rating("Low",    1.10F));
  modpRatings.addElement(new Rating("Nominal",  1.00F));
  modpRatings.addElement(new Rating("High",    0.91F));
  modpRatings.addElement(new Rating("Very high", 0.82F));
  // create the Modern Practices CostDriver and add it to
  // the vector
  costDrivers.addElement(new CostDriver("Modern Practices",
      modpRatings));
  // create a vector of Software Tools ratings
  Vector toolRatings = new Vector(5);
  toolRatings.addElement(new Rating("Very low",  1.24F));
  toolRatings.addElement(new Rating("Low",    1.10F));
  toolRatings.addElement(new Rating("Nominal",  1.00F));
  toolRatings.addElement(new Rating("High",    0.91F));
  toolRatings.addElement(new Rating("Very high", 0.83F));
  // create the Software Tools CostDriver and add it to
  // the vector
  costDrivers.addElement(new CostDriver("Software Tools",
      toolRatings));
  // create a vector of Schedule Constraints ratings
  Vector scedRatings = new Vector(5);
  scedRatings.addElement(new Rating("Very low",  1.23F));
  scedRatings.addElement(new Rating("Low",    1.08F));
  scedRatings.addElement(new Rating("Nominal",  1.00F));
  scedRatings.addElement(new Rating("High",    1.04F));
  scedRatings.addElement(new Rating("Very high", 1.10F));
  // create the Schedule Constraints CostDriver and add
  // it to the vector
  costDrivers.addElement(new CostDriver(
      "Schedule Constraints",  scedRatings));
}

After the costDrivers vector has been loaded, a for loop is used to iterate through all but the last cost driver on this panel. The last driver is skipped because it will be displayed using checkboxes. To do this, the last element in the costDrivers vector is cast into the variable cd as shown here:

CostDriver cd = (CostDriver)costDrivers.elementAt(qty-1);

Next, cbVector is allocated to hold up to six elements, the most required by any COCOMO cost driver. After adding a label to the display, the variable itemsOnLine is set to 0. This variable will be used to count how many checkbox objects have been placed on the current line so that the layout manager can be correctly told when to move to the next line in the layout.

After a new CheckboxGroup object is constructed and placed in cbGroup, the GridBagConstraints are set to WEST. This means that items will be aligned along their left edges. Because this constraint holds true for all the checkbox items, the constraint is set outside the loop. The loop uses an enumeration to move through the ratings associated with this cost driver. Before each new checkbox is added, checks are made to determine whether the current item is the default item (in which case its name is Nominal) and to ensure that no more than three checkboxes are on any line.

ProjectPanel is a subclass of CostDriverPanel, and therefore it provides implementations of getMultiplier, SetDefaults, and GetSummary. Each of these methods is similar to the equivalent method in ChoicePanel, with the addition of retrieving or setting a value in a checkbox. Because these methods are so similar, they are not described here but are included on the CD that accompanies this book.

The Results Panel

The third area of the CostEstimator user interface is the results panel. As you saw in Figure 23.4, the results panel is displayed at the lower left of the applet's main screen. Creating the results panel is a relatively straightforward process. To maintain precise control over the placement of objects on the screen, however, a GridBagLayout layout manager is used. This makes the CreateResultsPanel method, shown in Listing 23.9, fairly long.


Listing 23.9. The CreateResultsPanel method in CostEstimator.
private void CreateResultsPanel(GridBagLayout parentLayout)
{
  // create a panel and add it to the applet using the
  // applet's layout manager
  Panel p = new Panel();
  GridBagConstraints gbc = new GridBagConstraints();
  gbc.gridwidth = GridBagConstraints.RELATIVE;
  gbc.fill = GridBagConstraints.BOTH;
  parentLayout.setConstraints(p, gbc);
  add(p);
  // create and assign a GridBagLayout as the layout manager
  // for this panel
  GridBagLayout layout = new GridBagLayout();
  p.setLayout(layout);
  // place some space at the top of the panel
  Panel space = new Panel();
  gbc.gridheight =3;
  gbc.weighty =1;
  gbc.gridwidth = GridBagConstraints.REMAINDER;
  gbc.fill = GridBagConstraints.BOTH;
  layout.setConstraints(space, gbc);
  p.add(space);
  gbc.weighty = 0;
  // create a Label and TextField for KLOC
  Label klocLabel = new Label("KLOC:");
  gbc.gridheight = 1;
  gbc.gridwidth = GridBagConstraints.RELATIVE;
  layout.setConstraints(klocLabel, gbc);
  p.add(klocLabel);
  gbc.gridwidth = GridBagConstraints.REMAINDER;
  klocField = new TextField(5);
  layout.setConstraints(klocField, gbc);
  p.add(klocField);
  // create a Label and TextField for Effort Adjustment
  Label effortAdjustLabel = new Label("Effort Adjustment:");
  gbc.gridwidth = GridBagConstraints.RELATIVE;
  layout.setConstraints(effortAdjustLabel, gbc);
  p.add(effortAdjustLabel);
  gbc.gridwidth = GridBagConstraints.REMAINDER;
  effortAdjustmentField = new TextField(5);
  // make the field read-only
  effortAdjustmentField.setEditable(false);
  layout.setConstraints(effortAdjustmentField, gbc);
  p.add(effortAdjustmentField);
  // create a Label and TextField for Person Months
  Label personMonthsLabel = new Label("Person Months:");
  gbc.gridwidth = GridBagConstraints.RELATIVE;
  layout.setConstraints(personMonthsLabel, gbc);
  p.add(personMonthsLabel);
  gbc.gridwidth = GridBagConstraints.REMAINDER;
  personMonthsField = new TextField(5);
  // make the field read-only
  personMonthsField.setEditable(false);
  layout.setConstraints(personMonthsField, gbc);
  p.add(personMonthsField);
  // create a Label and TextField for Calendar Months
  Label calendarMonthsLabel = new Label("Calendar Months:");
  gbc.gridwidth = GridBagConstraints.RELATIVE;
  layout.setConstraints(calendarMonthsLabel, gbc);
  p.add(calendarMonthsLabel);
  gbc.gridwidth = GridBagConstraints.REMAINDER;
  calendarMonthsField = new TextField(5);
  // make the field read-only
  calendarMonthsField.setEditable(false);
  layout.setConstraints(calendarMonthsField, gbc);
  p.add(calendarMonthsField);
  // create some space at the bottom of the panel
  // so the fields above are properly located
  space = new Panel();
  gbc.gridheight =3;
  gbc.weighty =1;
  gbc.gridwidth = GridBagConstraints.REMAINDER;
  gbc.fill = GridBagConstraints.BOTH;
  layout.setConstraints(space, gbc);
  p.add(space);
}

The key to understanding CreateResultsPanel is understanding the various panels that are used. Overall, a panel is created and added to the applet. This panel is the full size of the results area of the user interface. Both this panel and the applet it is placed on use GridBagLayout as their layout manager. On this large panel, the following smaller panels are placed:

The empty panels are both created with the following GridBagConstraints:

gbc.gridheight =3;
gbc.weighty =1;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.BOTH;

These constraints allow the empty panels to fill up the vertical space evenly, thereby centering the panel that contains the labels and text fields.

Creating the Button Panel

The final part of the CostEstimator applet's user interface is the set of three buttons at the bottom right of the applet's main screen. As shown in Figure 23.4, the buttons are labeled Summary, Recalc, and Reset. These buttons are created by the CreateButtonPanel method shown in Listing 23.10.


Listing 23.10. The CreateButtonPanel method in CostEstimator.
private void CreateButtonPanel(GridBagLayout parentLayout)
{
  // create a panel and add it to the applet using the
  // applet's layout manager
  Panel p = new Panel();
  GridBagConstraints gbc = new GridBagConstraints();
  gbc.gridwidth = GridBagConstraints.REMAINDER;
  parentLayout.setConstraints(p, gbc);
  add(p);
  // create and assign a GridBagLayout as the layout manager
  // for this panel
  GridBagLayout layout = new GridBagLayout();
  p.setLayout(layout);
  // place some space above the buttons
  Panel space = new Panel();
  gbc.gridheight =3;
  gbc.weighty =1;
  gbc.gridwidth = GridBagConstraints.REMAINDER;
  gbc.fill = GridBagConstraints.BOTH;
  layout.setConstraints(space, gbc);
  p.add(space);
  // place some space to the left of the buttons
  space = new Panel();
  gbc.gridheight = 4;
  gbc.weighty = 1;
  gbc.gridwidth = GridBagConstraints.RELATIVE;
  gbc.weightx=1;
  layout.setConstraints(space, gbc);
  p.add(space);
  gbc.gridheight = 1;
  gbc.gridwidth = GridBagConstraints.REMAINDER;
  // add the Summary button
  gbc.fill = GridBagConstraints.HORIZONTAL;
  gbc.weightx =1;
  gbc.anchor = GridBagConstraints.EAST;
  Button b = new Button("Summary");
  layout.setConstraints(b, gbc);
  p.add(b);
  // add the Recalc button
  b = new Button("Recalc");
  layout.setConstraints(b, gbc);
  p.add(b);
  // add the Reset button
  b = new Button("Reset");
  layout.setConstraints(b, gbc);
  p.add(b);
}

CreateButtonPanel works much like CreateResultsPanel. First, a panel is constructed and placed onto the applet. Then a panel named space is constructed and placed across the top of the large panel. Next, another panel is constructed and also stored in the space variable. This panel is then assigned the following GridBagConstraints:

gbc.gridheight = 4;
gbc.weighty = 1;
gbc.gridwidth = GridBagConstraints.RELATIVE;
gbc.weightx=1;

These constraints will cause the panel to be aligned vertically along the left edge of the panel, beginning just below the previously placed panel. This is shown in Figure 23.6. After the empty panels have been placed, the three buttons are constructed and added.

Figure 23.6 : Two empty panels are placed on a larger panel to create the Button panel.

The CostEstimator Buttons

To respond to button presses, it is necessary to include an action method in the CostEstimator class. In this case the action method checks the label of the button and then simply calls an appropriate method based on which button was pressed. This is carried out in the following way:

public boolean action(Event evt, Object obj)
{
  boolean result=false;
  if("Reset".equals(obj))
  {
    DoReset();
    result = true;
  }
  else if("Recalc".equals(obj))
  {
    DoRecalc();
    result = true;
  }
  else if("Summary".equals(obj))
  {
    ShowSummary();
    result = true;
  }
  return result;
}

Resetting Default Values

If the Reset button has been pressed, the DoReset method is called. This method is written as follows:

private void DoReset()
{
  // use an enumeration to move through the CostDriverPanels
  for (Enumeration enum = CostDriverPanelVector.elements();
      enum.hasMoreElements(); )
  {
    // for each panel, call SetDefaults
    CostDriverPanel p = (CostDriverPanel)enum.nextElement();
    p.SetDefaults();
  }
}

DoReset uses the CostDriverPanelVector that contains a reference to each of the CostDriverPanels. This vector includes three ChoicePanel objects (for the product, computer, and personnel cost driver categories) and one ProjectPanel object. An enumeration is used to move through the vector, and for each item the SetDefaults method is called. Because SetDefaults was declared as abstract in CostDriverPanel and was overridden in ChoicePanel and ProjectPanel, the implementations of SetDefaults in these subclasses will be called.

Estimating the Project Schedule

Selecting the Recalc button causes the DoRecalc method to be executed. This method is shown in Listing 23.11. DoRecalc reads the user's entry from the klocField text field. This field should contain the number of lines of code in thousands. Because klocField might be blank or contain non-numeric entries, however, the call to Float.valueOf(klocField.getText()) is enclosed in a try...catch block that will catch the exception NumberFormatException that could be thrown.


Listing 23.11. The DoRecalc method in the CostEstimator class.
private void DoRecalc()
{
  try
  {
    // make sure a number was entered in klocField
    Float kloc = Float.valueOf(klocField.getText());
    // for this level of semidetached COCOMO, start with
    // 1.12 as the base effort adjustment factor
    float effortAdjustment = 1.12F;
    // calculate the Effort Adjustment Factor by calling
    // getMultiplier for each CostDriverPanel
    for (Enumeration e=CostDriverPanelVector.elements();
        e.hasMoreElements(); )
    {
      CostDriverPanel p = (CostDriverPanel)e.nextElement();
      effortAdjustment *= p.getMultiplier();
    }
    // display the Effort Adjustment Factor
    effortAdjustmentField.setText(String.valueOf(
        effortAdjustment));
    // calculate and display the number of person months
    // on the project
    int personMonths=(int)(3.0D*(Math.pow(kloc.floatValue(),
        effortAdjustment)));
    personMonthsField.setText(String.valueOf(personMonths));
    // calculate and display the number of calendar months
    // on the project
    double calendarMonths=2.5*(Math.pow(personMonths, 0.35F));
    calendarMonthsField.setText(String.valueOf(Math.round(
        calendarMonths)));
  }
  catch (NumberFormatException e)
  {
    // if klocField doesn't contain a number, set all
    // the fields to blank
    effortAdjustmentField.setText("");
    personMonthsField.setText("");
    calendarMonthsField.setText("");
  }
}

Next, the Effort Adjustment Factor is calculated by setting the variable effortAdjustment to 1.12 initially and then multiplying effortAdjustment by the multiplier calculated for each cost driver category. After the Effort Adjustment Factor is calculated, it is displayed in the effortAdjustmentField with the following code:

effortAdjustmentField.setText(String.valueOf(effortAdjustment));

After the Effort Adjustment Factor has been calculated, the estimated person months and calendar months are calculated. These calculations are performed according to the intermediate COCOMO equations described earlier in this chapter. When calculated, each of these fields is displayed using setText.

Summarizing Project Attributes

The CostEstimator applet's final button is the Summary button. Pressing this button causes a new instance of the SummaryFrame class to be constructed and shown. This is achieved by the method ShowSummary, which appears as shown here:

private void ShowSummary()
{
  SummaryFrame frame=new SummaryFrame(CostDriverPanelVector);
  frame.show();
}

The SummaryFrame Class

The SummaryFrame class is used to display text summarizing the selections the user has made describing the project being estimated. Figure 23.7 shows a sample SummaryFrame and examples of the text that could appear within it. As you can see in this figure, for each cost driver the name of the cost driver is given, along with the user's selection and the multiplier associated with that selection.

Figure 23.7 : A SummaryFrame summarizes the cost driver selections.

The SummaryFrame class, shown in Listing 23.12, is a subclass of Frame. The SummaryFrame constructor is passed a vector named CostDriverPanelVector. This is the vector that contains an element for each category panel. An enumeration is used to move through each entry in this vector. For each item the GetSummary method is invoked. The string returned by GetSummary is appended to the text area that is displayed on the SummaryFrame.


Listing 23.12. The SummaryFrame class.
class SummaryFrame extends Frame {
  private TextArea Summary; // text area for displaying results
  public SummaryFrame(Vector CostDriverPanelVector)
  {
    super("Attibute Summary");
    // create the TextArea for displaying the results
    Summary = new TextArea(10, 25);
    add("Center", Summary);
    // use an enumeration to move through each
    // CostDriverPanel
    for (Enumeration enum = CostDriverPanelVector.elements();
        enum.hasMoreElements(); )
    {
      // for each CostDriverPanel, use the GetSummary method
      // to retrieve a String summarizing the values on
// that panel
      CostDriverPanel p = (CostDriverPanel)enum.nextElement();
      Summary.appendText(p.GetSummary());
    }
    // create a panel and put a Close button on it at the
    // bottom of the frame
    Panel p = new Panel();
    p.add(new Button("Close"));
    add("South", p);
    resize(250, 400);
  }
  public boolean action(Event evt, Object arg)
  {
    boolean result = false;
    // if the Close button is pressed, dispose of the frame
    if("Close".equals(evt.arg)) {
      dispose();
      result = true;
    }
    return result;
  }
}

A Close button is added to the frame. The action method of the SummaryFrame class watches for a press of this button. When the Close button is detected, the SummaryFrame is disposed of.

Summary

In this chapter you combined much of what you learned in previous chapters to create a useful real-world applet. The CostEstimator involved some in-depth user interface programming and provided opportunities to work with vectors and arrays. You also learned about imagemaps and how to use an imagemap to control the appearance of an applet.

You should not stop here, however. You could extend this applet in many ways. For example, you could create a new subclass of CostDriverPanel that allows data entry with scrollbars. Or you could draw a pie graph showing the relative impact of each cost driver. You could use the Data Access Objects to store projects in a database and then support comparisons between different projects.