All Categories :
Java
Chapter 14
Advanced Image Processing
CONTENTS
This chapter's project, one that views the Mandelbrot set, gives
you examples of some of the more advanced concepts you have been
introduced to. The Mandelbrot set is the most spectacular example
of fractals, which represents one of the hot scientific topics
of recent years. With the applets in this chapter, you can view
or generate an original Mandelbrot image and zoom in and out of
it to produce new portions of the set.
Since the Mandelbrot set can take a while to generate-requiring
millions of calculations-it gives you a chance to combine threads
and image filters so you can view the set as it's being generated.
You might also want to save the Mandelbrot images. The BmpClass,
introduced in Part III, that converts a BMP formatted file into
a Java image, is enhanced so you can save the Mandelbrot data
as a BMP file. You can then view or modify it with any tool that
can handle the BMP format. Finally, the chapter concludes by showing
how you can auto-document the source of a Java class into a HTML
file. This can be viewed by a browser and has links to other classes.
Since you have already been introduced to most aspects of Java,
this chapter will jump straight into the project. Topics will
be introduced as appropriate.
There are actually two applets in this chapter. The first applet,
MandelApp, is used to generate a full Mandelbrot set. The tools
and BMP file produced by this applet are input into the second
applet, called MandelZoomApp. This applet displays a Mandelbrot
set, then allows you to zoom (magnify) portions of the set so
you can inspect its fractal qualities. You can also return to
previous images and zoom into another area.
If you want to use the file-saving capabilities of this program,
you need to run it from something that does not prevent file saving,
such as the appletviewer program. You can run the program in a
browser like Netscape, though; it will be able to do everything
except save the images as files.
Table 14.1 lists the classes used in this chapter's applets. Most
of the classes are new, so their names are set in boldface type.
Existing classes that were modified have their names italicized.
Table 14.1. Mandelbrot project classes and interfaces.
| Class/Interface | Description
|
| BmpImage | For BMP-Image conversion.
|
| CalculatorFilter | ImageFilter that produces updates of images as they are generated.
|
| CalculatorFilterNotify | Interface that defines ways an ImageFilter can receive data updates.
|
| CalculatorImage | Used to tie a calculation object, an image, and a CalculatorFilter together.
|
| CalculatorProducer | Interface that defines a mechanism for establishing how an ImageFilter can update a calculation class.
|
| MandelApp | An Applet that produces a full Mandelbrot image and lets you save it to a file.
|
| MandelEntry | Accessor class for keeping information about a Mandelbrot image.
|
| Mandelbrot | A Thread that produces Mandelbrot data for the specified parameters. It implements CalculatorProducer to get started by a filter. It uses CalculatorFilterNotify to update a filter with new data.
|
| MandelZoomApp | An Applet that displays the full Mandelbrot set and allows you to zoom in and out of the set.
|
Because the Mandelbrot set can take quite a while to generate,
it was designed by combining a calculation thread with an image
filter so you can see the results as they are generated. However,
understanding how the classes interrelate is a little tricky.
Figure 14.1 shows the workflow involved in producing a Mandelbrot
image. Understanding this flow is the key to understanding this
project.
Figure 14.1 : Workflow of producing a Mandelbrot image.
The process begins when an applet displaying Mandelbrot sets constructs
a Mandelbrot object. (In this project, the two Applet classes
are MandelApp and MandelZoomApp.) The Mandelbrot object, in turn,
creates an instance of the CalculatorImage class. The Mandelbrot
set passes itself as a part of the CalculatorImage constructor.
It is referenced as a CalculatorProducer object, an interface
that the Mandelbrot class implements. This interface implementation
will be used to communicate with the image filter.
In the next step, the applet requests a Mandelbrot image. This
is initiated by calling the getImage()
method of the Mandelbrot object, which in turn leads to a call
to a like-named method of the CalculatorImage object. At this
point, the CalculatorImage object first creates a color palette
by using an instance of the ImageColorModel class, then creates
a MemoryImageSource object. This object, which implements ImageProducer,
produces an image initialized to all zeros (black); it's combined
with an instance of the CalculatorFilter class to produce a FilteredImageSource.
When the MemoryImageSource object produces its empty image, it
is passed to the CalculatorFilter, which takes the opportunity
to produce the calculated image. It does this by kicking off the
thread of the image to be calculated. The CalculatorFilter doesn't
know that it is the Mandelbrot set that's calculated-it just knows
that some calculation needs to occur in the CalculatorProducer
object in which it has a reference.
Once the Mandelbrot thread is started, it begins the long calculations
to produce a Mandelbrot set. Whenever it finishes a section of
the set, it notifies the filter with new data through the CalculatorFilterNotify
interface. The filter, in turn, lets the viewing applet know that
it has new data to display by updating the corresponding ImageConsumer,
which causes the applet's imageUpdate()
method to be called. This causes a repaint, and the new image
data to be displayed. This process repeats until the full image
is created.
As you have probably observed, this is a complicated process.
Although the mechanics of image processing were introduced in
Part III, it doesn't hurt to have another example. The Calculator
classes here are meant to provide a generic approach toward manipulating
images that need long calculations. You can replace the Mandelbrot
class with some other calculation thread that implements CalculatorProducer,
and everything should work. A good exercise would be to replace
Mandelbrot with another fractal calculation or some other scientific
imaging calculation (I found that replacing Mandelbrot with a
Julia fractal class calculation was very easy).
Before going into the internals of the classes that make up this
project, it's worth spending a couple of moments to understand
what's behind the images produced by the Mandelbrot class.
In the 1970s, Benoit Mandelbrot at IBM was using computers to
study curves generated by iterations of complex formulas. He found
that these curves had unusual characteristics, one of which is
called self-similarity. The curves have a series of patterns
that repeat themselves when inspected more closely.
One of the characteristics of the curves Mandelbrot studied was
that they could be described as having a certain dimensional quality
that Mandelbrot termed "fractal." One of the fractals
that Mandelbrot was investigating is called a Julia set.
By mapping the set in a certain way, Mandelbrot came across a
set that turned out to include all the Julia sets-a kind of a
master set that was deemed the Mandelbrot set. This set
has several spectacular features, all of them beautiful. The most
striking of these is its self-similarity and a extraordinary sensitivity
to initial conditions. As you explore the Mandelbrot set, you
will be amazed by both its seeming chaos and exquisite order.
Figure 14.2 shows the famous Mandelbrot set, produced by this
chapter's MandelApp applet. The figures in this chapter show the
kind of images that appear when you zoom into various places in
this set. The Mandelbrot set is based on a seemingly simple iterated
function, shown in Formula 14.1.
Figure 14.2 : The full Mandelbrot set image.
Formula 14.1. Formula for calculating the Mandelbrot set.
zn+1=zn2
+ c
In Formula 14.1, z and c
are complex numbers. The Mandelbrot set is concerned with what
happens when z0 is zero and
c is set over a range of
values. The real part of c
is set to the x-axis, and the complex portion corresponds to the
y-axis. A color is mapped to each point based on how quickly the
corresponding value of c
causes the iteration to reach infinity. The process of "zooming"
in and out of the Mandelbrot set is equivalent to defining what
ranges of c are going to
be explored. It is amazing that something so simple can yield
patterns so sophisticated!
| Note |
If you are more interested in chaos and fractals, there are a lot of places to turn. Chaos by James Gleick (Penguin, 1987) is a layman's introduction to the ideas and discoveries that gave rise to chaos theory and the study of fractals. Mandelbrot's The Fractal Geometry of Nature (W.H. Freeman, 1983) lays out his ideas on fractals and nature. For a rigorous mathematical treatment of fractals, see the beautiful book Fractals Everywhere (Academic Press, 1988), written by one of the foremost figures in fractals, Michael Barnsley. Among other things, Barnsley is a major innovator on how to use fractal geometrics to achieve high rates of data compression. For a no-nonsense approach to writing programs that display fractals, see Fractal Programming in C by Roger T. Stevens (M&T Books, 1989). The algorithms for the Mandelbrot set were developed from this book. The C programs in this book map very easily to Java-except for the underlying graphics tools, which were developed for MS-DOS. However, the image calculation classes created in this chapter aim to fill this gap. With Stevens's book and these classes, you should be able to move his C code right over to Java and begin exploring the amazing world of fractals!
|
There are two applets in this chapter. The first applet, MandelApp,
generates the full Mandelbrot set. This will take a little while,
depending on your computer; for example, on a 486DX2-50 PC, it
takes a couple of minutes. When the image is complete, indicated
by a message on the browser's status bar, you can save the image
to a BMP formatted file by clicking anywhere on the applet's display
area. The file will be called mandel.bmp. Remember to run this
applet from a program, such as appletviewer, that lets applets
write to disk.
The other applet, MandelAppZoom, is more full-featured. It begins
by loading the Mandelbrot bitmap specified by an HTML applet parameter
tag. The default mandel1
corresponds to a BMP file and a data file that specifies x-y parameter
values-included on this book's CD-ROM.
Once the image is up, you can pick regions to zoom in on by clicking
on a point in the image, then dragging the mouse to the endpoint
of the region you want to display. Enter z
or Z on the keyboard, and
the applet creates the image representing the new region of the
Mandelbrot set. The key to this applet is patience! The calculations
can take a little while to set up and run. The applet tries to
help your patience by updating the status bar to indicate what
is going on. Furthermore, the image filter displays each column
of the set as the calculations advance.
You might select a region that doesn't appear to have anything
interesting to show when you zoom in on it. You can stop a calculation
in the middle by entering a
or A on the keyboard. The
applet will take a moment to wrap up, but then you can proceed.
When you are having problems finding an interesting region to
look at, try increasing the size of the highlighted area. This
will yield a bigger area that is generated, giving you a better
feel for what should be inspected. You get the best results by
working with medium-sized highlighted regions, rather than large
or small ones.
Figures 14.3 to 14.6 show what some of the zoomed-in regions of
the Mandelbrot set look like. Figure 14.3 is a large area picked
above the black "circles" of the full Mandelbrot set;
Figure 14.5 explores an area between two of the black areas. The
richest displays seem to occur at the boundaries of the black
areas. The black color indicates that the particular value takes
a long time to reach infinity. Consequently, these are also the
regions that take the longest to calculate. You get what you pay
for!
Figure 14.3 : Zoom in over block regions of Figure 14.2.
Figure 14.4 : Zoom in of Figure 14.3.
The zoom applet maintains a cache of processed images so you can
move back and forth among the processed images. Table 14.2 lists
the text codes for using the zoom applet.
Figure 14.5 : Zoom in between black regions of Figure 14.2.
Figure 14.6 : Zoom in of Figure 14.5.
Table 14.2. Codes for controlling the Mandelbrot applet.
| Characters | Action
|
| A or a
| Abort current Mandelbrot calculation. |
| B or b
| Go to previous image. |
| F or f
| Go to next image. |
| C or c
| Remove all but full image from memory. |
| N or n
| Go to next image. |
| P or p
| Go to previous image. |
| S or s
| Save the current image to a BMP file prefixed by tempMandel.
|
`| Z or z
| Zoom in on currently highlighted region. |
The Mandelbrot class, shown in Listing 14.1, calculates the Mandelbrot
set. It implements the Runnable interface, so it can run as a
thread, and also implements the CalculatorProducer interface,
so it can update an image filter of progress made in its calculations.
There are two constructors for the Mandelbrot class. The default
constructor produces the full Mandelbrot set and takes the dimensions
of the image to calculate. The Real and Imagine variables in the
constructors and the run()
method are used to map the x-y axis to the real and imaginary
portions of c in Formula
14.1. The other constructor is used to zoom in on a user-defined
mapping.
A couple of the other variables are worth noting. The variable
maxIterations represents
when to stop calculating a number. If this number, set to 512,
is reached, then the starting value of c
takes a long time to head toward infinity. The variable maxSize
is a simpler indicator of how quickly the current value grows.
How the current calculation is related to these variables is mapped
to a specific color; the higher the number, the slower the growth.
If you have a fast computer, you can adjust these variables to
get a richer or duller expression of the Mandelbrot set.
Once the thread is started (by the CalculatorFilter object through
the start() method), the
run() method calculates the
Mandelbrot values and stores a color corresponding to the growth
rate of the current complex number into a pixel array. When a
column is complete, it uses the CalculateFilterNotify to let the
related filter know that new data has been produced. It also checks
to see whether you want to abort the calculation. Note how it
synchronizes the stopCalc
boolean object in the run()
and stop() methods.
The calculation can take a while to complete. Still, it takes
only a couple of minutes on a 486-based PC. This performance is
quite a testament to Java! With other interpreted, portable languages
you would probably be tempted to use the reset button because
the calculations would take so long. With Java you get fast visual
feedback on how the set unfolds.
A good exercise is to save any partially developed Mandelbrot
set; you can use the saveBMP()
method here. You also need some kind of data file to indicate
where the calculation was stopped.
Listing 14.1. The Mandelbrot class.
import java.awt.image.*;
import java.awt.Image;
import java.lang.*;
// Class for producing a Mandelbrot set image...
public class Mandelbrot implements Runnable, CalculatorProducer
{
int width; // The dimensions of the
image...
int height;
CalculateFilterNotify filter; // Keeps track
of image production...
int pix[]; // Pixels used to construct image...
CalculatorImage img;
// General Mandelbrot parameters...
int numColors = 256;
int maxIterations = 512;
int maxSize = 4;
double RealMax,ImagineMax,RealMin,ImagineMin; //
Define sizes to build...
private Boolean stopCalc = new Boolean(false); //
Stop calculations...
// Create standard Mandelbrot set
public Mandelbrot(int width,int height) {
this.width = width;
this.height = height;
RealMax = 1.20; //
Default starting sizes...
RealMin = -2.0;
ImagineMax = 1.20;
ImagineMin = -1.20;
}
// Create zoom of Mandelbrot set
public Mandelbrot(int width,int height,double
RealMax,double RealMin,
double ImagineMax,double ImagineMin) {
this.width = width;
this.height = height;
this.RealMax = RealMax; //
Default starting sizes...
this.RealMin = RealMin;
this.ImagineMax = ImagineMax;
this.ImagineMin = ImagineMin;
}
// Start producing the Mandelbrot set...
public Image getImage() {
img = new CalculatorImage(width,height,this);
return img.getImage();
}
// Start thread to produce data...
public void start(int pix[],CalculateFilterNotify
filter) {
this.pix = pix;
this.filter = filter;
new Thread(this).start();
}
// See if user wants to stop before completion...
public void stop() {
synchronized (stopCalc) {
stopCalc
= Boolean.TRUE;
}
System.out.println("GOT
STOP!");
}
// Create data here...
public void run() {
// Establish Mandelbrot parameters...
double Q[] = new double[height];
// Pixdata is for image filter
updates...
int pixdata[] = new int[height];
double P,diffP,diffQ, x, y,
x2, y2;
int color, row, column,index;
System.out.println("RealMax
= " + RealMax + " RealMin = " + RealMin +
"
ImagineMax = " + ImagineMax + " ImagineMin = "
+ ImagineMin);
// Setup calculation parameters...
diffP = (RealMax - RealMin)/(width);
diffQ = (ImagineMax - ImagineMin)/(height);
Q[0] = ImagineMax;
color = 0;
// Setup delta parameters...
for (row = 1; row < height;
row++)
Q[row] =
Q[row-1] - diffQ;
P = RealMin;
// Start calculating!
for (column = 0; column <
width; column++) {
for (row
= 0; row < height; row++) {
x
= y = x2 = y2 = 0.0;
color
= 1;
while
((color < maxIterations) &&
((x2
+ y2) < maxSize)) {
x2
= x * x;
y2
= y * y;
y
= (2*x*y) + Q[row];
x
= x2 - y2 + P;
++color;
}
//
plot...
index
= (row * width) + column;
pix[index]
= (int)(color % numColors);
pixdata[row]
= pix[index];
} // end
row
// Update
column after each iteration...
filter.dataUpdateColumn(column,pixdata);
P += diffP;
// See if
we were told to stop...
synchronized
(stopCalc) {
if
(stopCalc == Boolean.TRUE) {
column
= width;
System.out.println("RUN:
Got stop calc!");
}
} //
end sync
} // end col
// Tell filter that we're
done producing data...
System.out.println("FILTER:
Data Complete!");
filter.setComplete();
}
// Save the Mandelbrot set as a BMP file...
public void saveBMP(String filename) {
img.saveBMP(filename,pix);
}
}
The CalculateFilterNotify interface defines the methods needed
to update an image filter that works with a calculation thread.
As shown in Listing 14.2, the "data" methods are used
for conveying a new batch of data to the filter. The setComplete()
method indicates that the calculations are complete.
Listing 14.2. The CalculateFilterNotify interface.
/* Interface for defining methods for
updating a
Calculator Filter... */
public interface CalculateFilterNotify {
public void dataUpdate(); //
Update everything...
public void dataUpdateRow(int row); // Update
one row...
public void dataUpdateColumn(int col,int pixdata[]); //
Update one column...
public void setComplete();
}
The CalculatorProducer interface, as shown in Listing 14.3, defines
the method called when a calculation filter is ready to kick off
a thread that produces the data used to generate an image. The
CalculateFilterNotify object passed to the start()
method is called by the producer whenever new data is yielded.
Listing 14.3. The CalculatorProducer interface.
// Interface for a large calculation
to produce image...
interface CalculatorProducer {
public void start(int pix[],CalculateFilterNotify
cf);
}
The CalculatorFilter class in Listing 14.4 is a subclass of ImageFilter.
Its purpose is to receive image data produced by some long calculation
(like the Mandelbrot set) and update any consumer of the the new
data's image. The CalculatorProducer, indicated by variable cp,
is what produces the data.
Since the ImageFilter class was explained in detail in Part III,
issues related to this class are not repeated here. However, a
couple of things should be pointed out. When the image is first
requested, the filter gets the dimensions the consumer wants by
a call of the setDimensions()
method. At this point, the CalculatorFilter will allocate a large
array holding the color values for each pixel.
When the original ImageProducer is finished creating the original
image, the filter's imageComplete()
method will be called, but the filter needs to override this method.
In this case, the CalculatorFilter will start the CalculatorProducer
thread, passing it the pixel array to put in its updates. Whenever
the CalculatorProducer has new data, it will call one of the four
methods specified by the CalculateFilterNotify interface: dataUpdate(),
dataUpdateRow(), dataUpdateColumn(),
or setComplete(). (The dataUpdateColumn()
method is called by the Mandelbrot calculation since it operates
on a column basis.) In each of these cases, the filter updates
the appropriate consumer pixels by using the setPixels()
method, then calls the consumer's imageComplete()
method to indicate the nature of the change. For the three "data"
methods, the updates are only partial, so a SINGLEFRAMEDONE flag
is sent. The setComplete()
method, on the other hand, indicates that everything is complete,
so it sets a STATICIMAGEDONE flag.
Listing 14.4. The CalculatorFilter class.
import java.awt.image.*;
import java.awt.Image;
import java.awt.Toolkit;
import java.lang.*;
public class CalculatorFilter extends ImageFilter
implements CalculateFilterNotify {
private ColorModel defaultRGBModel;
private int width, height;
private int pix[];
private boolean complete = false;
private CalculatorProducer cp;
private boolean cpStart = false;
public CalculatorFilter(ColorModel cm,CalculatorProducer
cp) {
defaultRGBModel = cm;
this.cp = cp;
}
public void setDimensions(int width, int height)
{
this.width = width;
this.height = height;
pix = new int[width * height];
consumer.setDimensions(width,height);
}
public void setColorModel(ColorModel model)
{
consumer.setColorModel(defaultRGBModel);
}
public void setHints(int hints) {
consumer.setHints(ImageConsumer.RANDOMPIXELORDER);
}
public void resendTopDownLeftRight(ImageProducer
p) {
}
public void setPixels(int x, int y, int w, int
h,
ColorModel model, int pixels[],int
off,int scansize) {
}
public void imageComplete(int status) {
if (!cpStart) {
cpStart = true;
dataUpdate(); //
Show empty pixels...
cp.start(pix,this);
} // end if
if (complete)
consumer.imageComplete(ImageConsumer.STATICIMAGEDONE);
}
// Called externally to notify that more data
has been created
// Notify consumer so they can repaint...
public void dataUpdate() {
consumer.setPixels(0,0,width,height,
defaultRGBModel,pix,0,width);
consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
}
// External call to update a specific pixel
row...
public void dataUpdateRow(int row) {
// The key thing here is the second to
last parameter (offset)
// which states where to start getting
data from the pix array...
consumer.setPixels(0,row,width,1,
defaultRGBModel,pix,(width
* row),width);
consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
}
// External call to update a specific pixel
column...
public void dataUpdateColumn(int col,int pixdata[])
{
// The key thing here is the second to
last parameter (offset)
// which states where to start getting
data from the pix array...
consumer.setPixels(col,0,1,height,
defaultRGBModel,pixdata,0,1);
consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
}
// Called from external calculating program
when data has
// finished being calculated...
public void setComplete() {
complete = true;
consumer.setPixels(0,0,width,height,
defaultRGBModel,pix,0,width);
consumer.imageComplete(ImageConsumer.STATICIMAGEDONE);
}
}
The CalculatorImage class, shown in Listing 14.5, is the glue
between the CalculatorProducer class that produces the image data
and the CalculatorFilter that manages it. When an image is requested
with the getImage() method,
the CalculatorImage creates a color palette through an instance
of the ImageColorModel class, then creates a MemoryImageSource
object. This ImageProducer object produces an image initialized
to all zeros (black). It is combined with an instance of the CalculatorFilter
class to produce a FilteredImageSource. When the createImage()
method of the Toolkit is called, production of the calculated
image begins.
The color palette is a randomly generated series of pixel values.
Depending on your luck, these colors can be attractive or uninspiring.
The createPalette() method
is a good place to create a custom set of colors for this applet,
if you want to have some control over its appearance. You should
replace the random colors with hard-coded RGB values, and you
might want to download a URL file that specifies a special color
mapping.
Listing 14.5. The CalculatorImage class.
// This class takes a CalculatorProducer
and sets up the
// environment for creating a calculated image. Ties
the
// producer to the CalculatorFilter so incremental updates can
// be made...
public class CalculatorImage {
int width; // The dimensions of the
image...
int height;
CalculatorProducer cp; // What produces
the image data...
IndexColorModel palette; // The colors
of the image...
// Create Palette only once per session...
static IndexColorModel prvPalette = null;
int numColors = 256; // Number of
colors in palette...
// Use defines how big of an image they want...
public CalculatorImage(int width,int height,CalculatorProducer
cp) {
this.width = width;
this.height = height;
this.cp = cp;
}
// Start producing the Calculator image...
public synchronized Image getImage() {
// Hook into the filter...
createPalette();
ImageProducer p = new FilteredImageSource(
new MemoryImageSource(width,height,palette,
(new
int[width * height]),0,width),
new
CalculatorFilter(palette,cp));
// Return the image...
return Toolkit.getDefaultToolkit().createImage(p);
}
// Create a 256 color palette...
// Use Default color model...
void createPalette() {
// Create palette only once per session...
if (prvPalette != null) {
palette
= prvPalette;
return;
}
// Create a palette out of random RGB
combinations...
byte blues[], reds[], greens[];
reds = new byte[numColors];
blues = new byte[numColors];
greens = new byte[numColors];
// First and last entries are black and
white...
blues[0] = reds[0] = greens[0] = (byte)0;
blues[255] = reds[255] = greens[255]
= (byte)255;
// Fill in other entries...
for ( int x = 1; x < 254; x++ ){
reds[x] = (byte)(255 * Math.random());
blues[x] = (byte)(255 * Math.random());
greens[x] = (byte)(255 * Math.random());
}
// Create Index Color Model...
palette = new IndexColorModel(8,256,reds,greens,blues);
prvPalette = palette;
}
// Save the image set as a BMP file...
public void saveBMP(String filename,int pix[])
{
try {
BmpImage.saveBitmap(filename,palette,
pix,width,height);
}
catch (IOException ioe) {
System.out.println("Error
saving file!");
}
}
}
The MandelApp class, shown in Listing 14.6, creates and displays
the full Mandelbrot set; the end result is shown in Figure 14.2.
An instance of the Mandelbrot class is created in the init()
method. Whenever the Mandelbrot calculation has produced some
new data, it calls the ImageObserver-based method, imageUpdate().
This will probably result in the applet being repainted to show
the new data. If the image is complete, an internal flag is set.
After this, if you click the mouse, the image will be saved to
a BMP formatted file called mandel.bmp.
Listing 14.6. The MandelApp class.
import java.awt.*;
import java.lang.*;
import java.applet.Applet;
// This applet displays the Mandlebrot set through
// use of the Mandelbrot class...
public class MandelApp extends Applet {
Image im; // Image that displays
Mandelbrot set...
Mandelbrot m; // Creates the Mandelbrot image...
int NUMCOLS = 640; // Dimensions
image display...
int NUMROWS = 350;
boolean complete = false;
// Set up the Mandelbrot set...
public void init() {
m = new Mandelbrot(NUMCOLS,NUMROWS);
im = m.getImage();
}
// Will get updates as set is being created.
// Repaint when they occur...
public boolean imageUpdate(Image im,int flags,
int x, int y, int w, int h)
{
if ((flags & FRAMEBITS)
!= 0) {
showStatus("Calculating...");
repaint();
return true;
}
if ((flags & ALLBITS)
!= 0) {
showStatus("Image
Complete!");
repaint();
complete
= true;
return false;
}
return true;
}
// Paint on update...
public void update(Graphics g) {
paint(g);
}
public synchronized void paint(Graphics g) {
g.drawImage(im,0,0,this);
}
// Save Bitmap on mouse down when image complete...
public boolean mouseDown(Event evt,int x, int
y) {
if (complete)
{
showStatus("Save
Bitmap...");
m.saveBMP("mandel.bmp");
showStatus("Bitmap
saved!");
return
true;
} // end
if
return false;
}
}
Listing 14.7 shows the MandelZoomApp class, which represents this
chapter's main applet; its function was described earlier, in
the section "Using the Applets." See this section and
Table 14.1 for how to use the applet.
The most interesting features in the code are the routines for
marking the region to be highlighted. Each pixel on the displayed
Mandelbrot image maps an x-y value to a real-imaginary value of
the c value of the Mandelbrot
formula shown in Formula 14.1. Whenever you move the cursor, the
current real-imaginary values are shown in the browser's status
bar. When you highlight an area to zoom in on, you are really
picking a range of c values
to be explored. All the double variables are used for tracking
this range of values. These values are read in at initialization
by the loadParameters() method
to match the bitmap that's displayed. You can specify other Mandelbrot
BMP files and corresponding data files by changing the filename
parameter of the applet's <APPLET>
tag.
The Zoom() method takes the
currently highlighted range and brings up a new Mandelbrot image
that corresponds to this range. It uses the same calculation-image
filtering techniques that the MandelApp class does.
Listing 14.7. The MandelZoomApp class.
// This applet displays the Mandelbrot
set bitmap specified
// in the APPLET tag parameters. You can then zoom
and in
// and out of the bitmap by dragging a region to paint.
// And then clicking on the appropriate option...
// Z or z - Zoom
// S or s - Save.
public class MandelZoomApp extends Applet {
Image img;
boolean zoomOn = false;
double XLeft,XRight,YTop,YBottom,XDelta,YDelta;
double currentX,currentY;
double startX,startY,endX,endY; //
Zooming coordinates...
Rectangle markingRectangle; // Zooming
rectangle...
Mandelbrot m; // Creates the Mandelbrot image...
int NUMCOLS = 640; // Dimensions
image display...
int NUMROWS = 350;
boolean complete = false;
// Array for keeping track of Mandelbrot entries...
MandelEntry me[];
int lastIndex; // Top of array...
int currentIndex;
// Set up the Mandelbrot set specified in the
parameters...
public void init() {
img = null;
m = null;
// Get parameter of bitmap
to display...
String filename;
if ((filename = getParameter("filename"))
== null)
filename
= "mandel1";
// Load the bitmap...
loadBitmap(filename);
// Initialize Mandelbrot array...
me = new MandelEntry[40];
me[0] = new MandelEntry(null,img,XLeft,XRight,YTop,YBottom);
lastIndex = 0;
currentIndex = 0;
}
// ZOOM onto Mandelbrot set if all is good...
void Zoom() {
// No Zooming if off or no
rectangle...
if ((!zoomOn) || (markingRectangle
== null)) {
showMsg("Nothing
marked or Zooming disable...");
return;
} // end if
// See if Mandelbrot table
is full...
if ((lastIndex + 1) >=
me.length) {
showMsg("Mandelbrot
table full. Clear with C before zooming");
return;
}
showMsg("ZOOM: SX="
+ startX + " SY=" + startY + " EX=" + endX
+ " EY=" + endY);
// Load new Mandelbrot...
complete = false;
zoomOn = false;
markingRectangle = null; //
Reset marking rectangle...
m = new Mandelbrot(NUMCOLS,NUMROWS,endX,startX,
endY,startY);
img = m.getImage();
// Store in Mandelbrot table...
XLeft = startX;
XRight = endX;
YTop = startY;
YBottom = endY;
XDelta = Math.abs(XRight -
XLeft);
YDelta = Math.abs(YBottom
- YTop);
++lastIndex;
me[lastIndex] = new MandelEntry(m,img,startX,endX,startY,endY);
currentIndex = lastIndex;
showMsg("Calculating...");
repaint();
}
// Paint on update...
public void update(Graphics g) {
paint(g);
}
public synchronized void paint(Graphics g) {
if (img == null)
return;
// Show image...
g.drawImage(img,0,0,this);
// Show marking rectangle
if exists...
if (markingRectangle != null)
{
g.drawRect(markingRectangle.x,markingRectangle.y,
markingRectangle.width,markingRectangle.height);
} // end if
}
// Will get updates as set is being created.
// Repaint when they occur...
public boolean imageUpdate(Image im,int flags,
int x, int y, int w, int h)
{
if ((flags & FRAMEBITS)
!= 0) {
repaint();
return true;
}
if ((flags & ALLBITS)
!= 0) {
showMsg("Image
Complete!");
repaint();
complete
= true;
zoomOn =
true;
return false;
}
return true;
}
// Load a bitmap and accompanying data file...
void loadBitmap(String filename) {
// Zoom is false unless both
succeed...
zoomOn = false;
markingRectangle = null; //
Reset marking rectangle...
// Load the bitmap...
try {
showMsg("Load
image...");
ImageProducer
producer = BmpImage.getImageProducer(
getDocumentBase(),
filename + ".bmp");
img = createImage(producer);
showMsg("Image
loaded...");
}
catch (AWTException e){
img = null;
showMsg("Cannot
open file " + filename);
return;
}
// Load the zoom parameters.
// Turn Zoom on if all works...
try {
loadParameters(filename);
zoomOn =
true;
complete
= true;
}
catch (IOException e){
showMsg("Cannot
load parameter data. " + e.getMessage());
}
}
// Load the parameters. Throw IO
Exception...
public void loadParameters(String filename)
throws IOException {
// Create URL for data...
URL u;
try {
u = new URL(getDocumentBase(),filename
+ ".dat");
}
catch (MalformedURLException e) {
showMsg("Bad Data URL");
throw new IOException("Bad
URL");
}
// Now load the data by opening up a stream
// to the URL...
DataInputStream dis = new DataInputStream(
new BufferedInputStream(u.openStream()
) );
// Read only the first line...
String param = dis.readLine();
// Tokenize out the boundary values....
StringTokenizer s = new StringTokenizer(param,",");
try {
XLeft = Double.valueOf(s.nextToken()).doubleValue();
XRight = Double.valueOf(s.nextToken()).doubleValue();
YTop = Double.valueOf(s.nextToken()).doubleValue();
YBottom = Double.valueOf(s.nextToken()).doubleValue();
XDelta = Math.abs(XRight
- XLeft);
YDelta = Math.abs(YBottom
- YTop);
}
catch (NumberFormatException e) {
throw new IOException("Improperly
formatted data...");
}
catch (NoSuchElementException e) {
throw new IOException("Improperly
formatted data...");
}
}
// Track mouse to show fractal values and to
// mark area to zoom
public boolean handleEvent(Event evt) {
switch(evt.id) {
case Event.KEY_PRESS:
{
//
Z or z means Zoom
if
((evt.key == 'z') || (evt.key == 'Z'))
Zoom();
//
S or s means Save
if
((evt.key == 's') || (evt.key == 'S'))
saveFile();
//
A or a means Abort Zoom calculation...
if
((evt.key == 'a') || (evt.key == 'A')) {
if
(m != null) {
showMsg("Aborting
calculation...");
m.stop();
}
// end if
}
//
P or p means previous image...
if
((evt.key == 'p') || (evt.key == 'P'))
previousImage();
//
B or b means previous image...
if
((evt.key == 'B') || (evt.key == 'b'))
previousImage();
//
N or n means next image...
if
((evt.key == 'N') || (evt.key == 'n'))
nextImage();
//
F or f means next image...
if
((evt.key == 'F') || (evt.key == 'f'))
nextImage();
//
C or c means clear images
if
((evt.key == 'C') || (evt.key == 'c'))
clearImage();
return
true;
}
// Mouse
clicks. Start marking...
case Event.MOUSE_DOWN:
{
startMarking(evt.x,evt.y);
return
false;
}
case Event.MOUSE_DRAG:
{
dragMarking(evt.x,evt.y);
return
false;
}
case Event.MOUSE_UP:
{
stopMarking(evt.x,evt.y);
return
false;
}
case Event.MOUSE_MOVE:
{
showPosition(evt.x,evt.y);
return
false;
}
default:
return
false;
}
}
// Save the image as a file...
void saveFile() {
// Don't save if we are loading...
if (!complete)
return;
// Get Mandelbrot reference,
if exists...
Mandelbrot mb = me[currentIndex].getMandelbrot();
if (mb == null) {
showStatus("Cannot
save. Not generated in this session");
return;
} // end if
// Generate the filename...
String filename = "tempMandel"
+ (currentIndex + 1) + ".bmp";
// Security test...
try {
System.getSecurityManager().checkWrite(filename);
}
catch (SecurityException e)
{
showStatus("Write
not permitted!");
return;
}
// Save the image...
showStatus("Saving image
as " + filename + "...");
mb.saveBMP(filename);
showStatus("Image saved
as " + filename);
}
// Routines for moving through Mandelbrot table...
// Load previous image...
void previousImage() {
// Nothing if we are loading...
if (!complete)
return;
// Do nothing if at top index...
if (currentIndex == 0) {
showMsg("At
Top index");
return;
}
// Go to previous image...
reloadImage(currentIndex -
1);
}
// Load next image...
void nextImage() {
// Nothing if we are loading...
if (!complete)
return;
// Do nothing if at last index...
if (currentIndex == lastIndex)
{
showMsg("At
Last index");
return;
}
// Go to next image...
reloadImage(currentIndex +
1);
}
// Reload index from Mandelbrot array...
void reloadImage(int index) {
showMsg("Reloading image...");
currentIndex = index;
complete = true;
zoomOn = true;
markingRectangle = null; //
Reset marking rectangle...
// Get data from Mandelbrot
table...
img = me[currentIndex].getImage();
XLeft = me[currentIndex].getXLeft();
XRight = me[currentIndex].getXRight();
YTop = me[currentIndex].getYTop();
YBottom = me[currentIndex].getYBottom();
XDelta = Math.abs(XRight -
XLeft);
YDelta = Math.abs(YBottom
- YTop);
repaint();
}
// Remove everything but first image from stack...
void clearImage() {
for (int i = 1; i <= lastIndex;
++i)
me[i] =
null;
// Go back to first image...
lastIndex = 0;
reloadImage(0);
}
// ************************************
// Routines for mouse tracking...
// ************************************
boolean TrackingOn = false;
int leftX,topY;
// Start marking a zoom rectangle, erase existing
one...
void startMarking(int x,int y) {
// Get current positions...
Clear marking if invalid...
if (!showPosition(x,y)) {
TrackingOn
= false;
markingRectangle
= null;
repaint();
return;
} // end if
// Else, start marking...
TrackingOn = true;
startX = currentX;
startY = currentY;
endX = currentX;
endY = currentY;
leftX = x;
topY = y;
// Set marking rectangle and
repaint...
markingRectangle = new Rectangle(x,y,1,1);
repaint();
}
// Expand square of dragging unless invalid...
void dragMarking(int x,int y) {
// Get current positions...
Clear marking if invalid...
boolean good = showPosition(x,y);
// See if other marking conditions
hold.
// Such as going in a bad
direction...
if (good) {
if ((!TrackingOn)
|| (x < leftX) || (y < topY))
good
= false;
}
// Clear out if marking is
bad...
if (!good) {
TrackingOn
= false;
markingRectangle
= null;
repaint();
return;
} // end if
// Set new marking rectangle
and repaint...
endX = currentX;
endY = currentY;
markingRectangle = new Rectangle(leftX,topY,
x
- leftX,y - topY);
repaint(leftX,topY,markingRectangle.width
+ 1,
markingRectangle.height
+ 1);
}
// Stop marking...
void stopMarking(int x,int y) {
showPosition(x,y);
TrackingOn = false;
// Kill if too small...
if (markingRectangle != null)
{
if ((markingRectangle.width
< 3) ||
(markingRectangle.height
< 3)) {
markingRectangle
= null;
} // end
if
}
repaint();
}
// Show current position to status if an image
has
// been prepared...
// Returns true if good position, else bad...
boolean showPosition(int x,int y) {
// Return if not ready to
zoom...
if ((img == null) || (!zoomOn))
return false;
// See if we are in the display
area...
int width = img.getWidth(this);
int height = img.getHeight(this);
if ((x > width) || y >
height) {
showStatus("");
return false;
} // end if
currentX = XLeft + (XDelta
* (((double)x)/((double)width)));
currentY = YTop + (YDelta
* (((double)y)/((double)height)));
showStatus(currentX + "
: " + currentY);
return true;
}
// Print a message to standard out and the status
bar...
public void showMsg(String s) {
System.out.println(s);
showStatus(s);
}
}
A cache is maintained so that you can move back and forth between
images. The cache is an array of MandelEntry accessor objects;
the MandelEntry class is shown in Listing 14.8. The cache is set
to store up to 40 images. If you fill up the cache, press C
or c to clear the cache of
everything but the full Mandelbrot set image. As an exercise,
you might want to make the caching mechanism more sophisticated
so that it can bring in existing files, delete individual images,
and so forth.
Note that there is a little trick in the saveFile()
method. It uses the SecurityManager to see whether file writes
are allowed. This is a way of checking the browser's security
before a write is attempted. If file writing is prohibited, a
SecurityException is thrown. How this works will differ from browser
to browser.
Listing 14.8. The MandelEntry class.
// Store Mandelbrot images and corresponding
coordinates...
class MandelEntry {
Mandelbrot m;
Image img;
double XLeft,XRight,YTop,YBottom;
// Constructor: Store data...
public MandelEntry(Mandelbrot m,Image img,double
XLeft,
double XRight,double YTop,double
YBottom) {
this.m =
m;
this.img
= img;
this.XLeft
= XLeft;
this.XRight
= XRight;
this.YTop
= YTop;
this.YBottom
= YBottom;
}
// Accessor methods...
public Mandelbrot getMandelbrot() {
return m;
}
public Image getImage() {
return img;
}
public double getXLeft() {
return XLeft;
}
public double getXRight() {
return XRight;
}
public double getYTop() {
return YTop;
}
public double getYBottom() {
return YBottom;
}
}
The BmpImage class, introduced in Part III of this book, reads
images stored in the BMP format and converts them into a form
Java can use. In this chapter, more functions were added to the
class so that it could write the BMP back out to a file (shown
in Listing 14.9). It's basically the opposite of the reading process
discussed in Part III.
Listing 14.9. Adding BMP saving to the BmpImage class.
import java.lang.String;
import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.image.*;
/**
* This is a class that reads and writes a
* BMP formatted file
*/
public class BmpImage
{
// ... EXISTING CODE GOES HERE!!!
/**
* Write out a bitmap...
* Current only supporting 8 bits per
pixel...
* @String filename - The file to save
it as...
* @IndexColorModel ICM - Palette to use
* @int pix[] - Pixels to save
* @int width - Width of data
* @int height - Height of data
*/
public static void saveBitmap(String filename,
IndexColorModel ICM, int pix[],
int width, int height) throws IOException
{
// Create
output stream...
BmpImage
b = new BmpImage(filename);
DataOutputStream
os = new DataOutputStream(
new
BufferedOutputStream(
new FileOutputStream(filename) ) );
b.writeFileHeader(os,ICM,width,height);
b.write8bitWindowsHeader(os,ICM,width,height);
b.write8bitColorIndex(os,ICM);
b.write8bitData(os,pix,width,height);
os.close();
}
/**
* Write out the file header
* @DataOutputStream os - The output stream
to write
* @IndexColorModel ICM - Palette to use
* @int width - Width of data
* @int height - Height of data
*/
public void writeFileHeader(DataOutputStream
os,
IndexColorModel ICM,
int width, int height) throws IOException
{
byte b[] = new byte[4];
// Write out magic code...
b[0] = 'B';
b[1] = 'M';
os.write(b,0,2);
// Calculate size and offset..
int paletteSize = (ICM.getMapSize()
* 4);
int offset = 54 + paletteSize;
int fileSize = offset + (width
* height);
// Write out size & offset...
pushVal(os,fileSize,4);
pushVal(os,0,4);
pushVal(os,offset,4);
}
/**
* Write the bitmap header out to Windows...
* @DataOutputStream os - The output stream
to write
* @IndexColorModel ICM - Palette to use
* @int width - Width of data
* @int height - Height of data
*/
public void write8bitWindowsHeader(DataOutputStream
os,
IndexColorModel ICM,
int width, int height) throws IOException
{
pushVal(os,40,4); //
Bytes in header
pushVal(os,width,4); //
Size in pixels...
pushVal(os,height,4);
pushVal(os,1,2); //
# Color Planes
pushVal(os,8,2); //
Bits per pixel...
pushVal(os,0,4); //
NO Compression...
pushVal(os,width * height,4); //
Size of image...
// LATER PUT IN REAL DATA
pixels/meter
pushVal(os,3790,4); //
TBD: Horizontal Res pixels/meter
pushVal(os,3790,4); //
TBD: Vertical Res pixels/meter
pushVal(os,ICM.getMapSize(),4); // Indexes in bitmap...
pushVal(os,ICM.getMapSize(),4); //
Indexes in bitmap...
}
/**
* Write the bitmap header out to Windows...
* @DataOutputStream os - The output stream
to write
* @IndexColorModel ICM - Palette to use
*/
public void write8bitColorIndex(DataOutputStream
os,
IndexColorModel ICM) throws
IOException {
// Create
RGB array...
int paletteSize
= ICM.getMapSize();
byte blues[]
= new byte[paletteSize];
byte greens[]
= new byte[paletteSize];
byte reds[]
= new byte[paletteSize];
byte b[]
= new byte[4];
// Copy
RGB arrays...
ICM.getBlues(blues);
ICM.getGreens(reds);
ICM.getReds(reds);
// Write out palette...
for (int
i = 0; i < paletteSize; ++i) {
b[0]
= (byte)blues[i];
b[1]
= (byte)greens[i];
b[2]
= (byte)reds[i];
b[3]
= (byte)0;
os.write(b,0,4);
} // end
for
}
/**
* Write out data of bitmap...
* @DataOutputStream os - The output stream
to write
* @IndexColorModel ICM - Palette to use
* @int pix[] - Pixels to save
* @int width - Width of data
* @int height - Height of data
*/
public void write8bitData(DataOutputStream
os,
int pix[],int width, int height) throws
IOException {
// Bytes b...
byte b[] = new byte[width];
for (int i = 0; i < 4; ++i)
b[i] = 0;
// Calculate padding
int padding = 0;
int overage = width % 4;
if (overage != 0)
padding = 4 - overage;
// Write out starting from bottom of
height
int index,x,y;
for (y = (height - 1); y >= 0; -y)
{
// Write
out each row, send to big buffer...
index =
(y * width);
for (x =
0; x < width; ++x,++index)
b[x]
= (byte)pix[index];
// Write
out a big block...
os.write(b,0,width);
// Send
out padding...
if (padding
!= 0) {
for
(int i = 0; i < 4; ++i)
b[i]
= 0;
os.write(b,0,padding);
}
System.out.print(".");
System.out.flush();
} // end y for
System.out.println("Write done!");
}
/**
* Write out integer to little endian
stream...
* @DataOutputStream os - The output stream
to write
* @int data - Data to convert
* @int len - Length of array to convert
*/
private void pushVal(DataOutputStream
os,
int data,int len) throws IOException
{
byte b[] = new byte[len];
for (int i = 0; i < len;
++i) {
b[i] = (byte)(data
>> (i * 8));
}
os.write(b,0,len);
}
The last thing covered in this chapter is how to auto-document
your code. The tool javadoc,
provided with the JDK, can take a properly formatted file and
convert it into an HTML file, complete with links to the classes
it references (assuming those classes are also run through javadoc).
The BmpImage.java is such a file; its HTML output from javadoc
is shown in Figure 14.7 and is included on this book's CD-ROM.
Figure 14.7 : BmpImage HTML after being run through javadoc.
Comments that should appear in the HTML are marked by appearing
between /** and */,
as shown at the beginning of the source code in Listing 14.9.
The javadoc tool figures
out a lot of things on its own, such the superclass, what external
classes are used, and the individual parts of a method or variable
declaration. You can specify additional information for each method
by preceding certain keywords with an @.
These keywords include param,
returns, and exception
for documenting parameters, return values, and exceptions thrown,
respectively. For example, here is the declaration for getImageProducer():
/**
* A method to retreive an ImageProducer
given just a BMP URL.
* @param context contains the base URL
(from getCodeBase() or such)
* @returns an ImageProducer
* @exception AWTException on stream or
bitmap data errors
*/
Look at the HTML to see what this looks like.
You will need to place the HTML output in the right directory
for everything to work properly. You can also document an entire
package by passing the package name to javadoc.
The major work of this book began in Part II with a series of
discussions on how to use the AWT package. The spreadsheet applet
was used to show how to incrementally build an applet using AWT.
Since AWT is the basis for constructing the user interface of
your Java applets, you need to know the package well. While constructing
the spreadsheet, you also learned the subtleties of the java.io
package and how to create exception handlers. By the end of Part
II, you had seen most of the basics of Java applet programming.
Part III took everything a step further. You discovered the underlying
classes behind applets and images and saw how threads can be used
to enhance your application. Part III concluded with showing you
how AWT, the applet classes, images, and threads could be brought
together to create a Catalog applet, whose key element was a background
image loader running as a thread. Although this applet was not
a final product, it could-combined with the client/server mechanisms
of Part IV-be the basis for producing a serious catalog application
for use on the World Wide Web.
In Part IV, you incorporated the lessons of the previous chapters
to create the most complex application of the book. You saw how
to use the Java network classes to create a client-server application,
and how to use native methods to take full advantage of a particular
platform's features. The client applet demonstrated how Java could
be used to represent data as it changes in real time. The election
applet in Part IV was just the beginning of what you can create
with Java client-server technology.
Part V allowed you to focus on some of the more advanced features
of Java. You saw how HotJava, although still in its infancy, has
features that indicate the future of browsers and Java programming.
You also saw how to use Java's image classes to create advanced
animation and image-processing applets. With a knowledge of these
advanced imaging techniques, you can now use Java to produce images
more sophisticated than just a banner moving across the screen.
You have been given the tools to write great Java applets for
the Internet, the intranet, or anywhere else Java calls. Go for
it!

Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.