COMP 310
Spring 2010

Lec12:  Using Superclasses as Service Providers

Home  Info  Owlspace  Resources

Today, we will explore how superclasses can be used to provide invariant services to their subclasses.   The example we will use will be the advanced painting strategies used in the BallWar system.  For reference, please see the BallWar demo.   Also, see the BallWar documentation:

There are a lot of subtleties associated with this design that are not unique to painting, so it behooves you to study the implications of each feature very carefully and to carefully consider its larger meaning.

Here is a UML diagram that shows a possible architecture for providing services for a variety of different painting needs, such as arbitrary shapes, multi-point polygons, color-overriding strategies, images and animations.  Note that due to space limitations, only one of the concrete implementations for any branch of the hierarchy is shown in the diagram.    DO NOT COPY THIS DESIGN ROTE!   YOUR DESIGN MAY BE DIFFERENT!

Click the diagram for a full-size image
UML diagram of paint strategies

IPaintStrategy

The top-level superclass (interface, actually) that fundamentally defines what a painting strategy can do.

void paint(Graphics g, Ball host)  -- The actual paint operation that is called during the repainting process.  Paints the host onto the given Graphics context. The image is translated, scaled and rotated as determined by the host's location, radius, and velocity, i.e. it calculates its own affine transform.

void paintXfrm(Graphics g, Ball host, AffineTransform at) --  A secondary paint operation does the same thing as the above paint method, but does not calculate its own affine transform, but instead, uses a supplied affine transform.   This allows the same affine transform to be shared amongst paint strategies, reducing the number of times that it has to be calculated.

void init(Ball host) -- used to initialize the strategy and host ball, just as the update strategies do.

A simple, direct implementation of IPaintStrategy can be seen in SquarePaintStrategy, which does not rotate the square as it bounces around.   (Note: this code utilizes the fact that the Graphics object used by the current Java GUI system is in fact a Graphics2D subclass, which provides the useful fill method for drawing arbitrary filled Shape objects, e.g. Rectangles).  

APaintStrategy

This class provides the basic affine transform services that its subclasses will use to resize, translate and rotate their prototype images into their proper current locations and orientations on the screen. 

The constructor takes an java.awt.geom.AffineTransform object that is used to do all the affine transform calculations that are needed, saving the reference in a protected field for easy but protected access by its subclasses.  The instantiation of the AffineTransform object is left to the subclass because composite painting strategies will want to share the same instance.  

public void paint(Graphics g, Ball host) -- This Template design pattern method sets up AffineTransform object to translate, scale and rotate based on the the ball's current position, radius and velocity.   It then delegates  to two other methods to finish the job, the paintCfg method to finish any further refinements of the affine transform and the paintXfrm method to actually perform the painting.

  public void paint(Graphics g, Ball host) {
    double scale = host.getRadius();
    at.setToTranslation(host.getLocation().x, host.getLocation().y);
    at.scale(scale, scale);
    at.rotate(Math.atan2(host.getVelocity().y, host.getVelocity().x));
    g.setColor(host.getColor());    
    paintCfg(g, host);
    paintXfrm(g, host, at);
  }

The paintCfg method is set to be a concrete no-op that the subclasses may or may not override.  Since APaintStrategy doesn't know what sort of thing is being painted, it has no idea how exactly to apply the affine transform, so the paintXfrm method must remain abstract, forcing the subclasses to implement it.

 

ShapePaintStrategy

This class is very similar the SquarePaintStrategy, but abstracts out the java.awt.Shape painting process and provides rotation to orient the shape in the direction of the velocity as well.  It does this by leveraging off of the affine transform services provided by APaintStrategy, thus needing only to provide storage for an arbitrary shape and to implement the paintXfrm method:

  public void paintXfrm(Graphics g, Ball host, AffineTransform at){ 
    ((Graphics2D) g).fill(at.createTransformedShape(shape));
  }

All any subclass of ShapePaintStrategy needs to do is to supply a unit-sized Shape to paint.  

To create shapes, a hierarchy of factories is used to provide services (see the documentation for more specific descriptions):

BallWare Shape Factories

This is actually a design pattern called the Prototype Design Pattern, where prototypes are invariantly defined and then variantly scaled and/or otherwise manipulated for their specific uses.

PolygonFactory

The only differences between one polygon and another are the points of which they consist.  Hence the PolygonFactory's constructor is a vararg list that takes a set of Points used to construct a java.awt.Polygon instance.  

The easiest way to instantiate a Polygon is to first instantiate an "empty" Polygon with no points and then to call its addPoints methods to add points as x-y integer pairs.   Note that since Polygons are defined using integers, in order to create smoothly scalable shape, doubles must be used, hence the scale factors that are included in the PolygonFactory.

The process of making an instance of a polygon is simply a matter of

  1. setting the affine transform to translate the internal polygon to the desired location,

  2. setting the affine transform to a net scaling factor from the internal and supplied scale factors and

  3. to use the affine transform to take in internal unit polygon and return the desired translated and scaled shape.

  public Shape makeShape(double x, double y, double xScale, double yScale) {
    at.setToTranslation(x, y);
    at.scale(xScale*scaleFactor, yScale*scaleFactor);
    return at.createTransformedShape(poly);
  }

It is important to note that there are two notions of "unit shape" going here:

Fish1PolygonFactory and Fish2PolygonFactory simply call their superclass constructors with the necessary points to create their shapes.   NiceFishPaintStrategy is a MultiPaintStrategy that combines SwimFishPaintStrategy, Fish1PaintStrategy, Fish2PaintFactory, FixedColorDecoratorPaintStrategy and EllipsePaintStrategy.

MultiPaintStrategy

Just to show that it can be done, the multi-paint strategy here is implemented using an array of paint strategies, rather than as a binary tree.  By using a vararg constructor, as many strategies as desired can be combined at once, not just two.  

QUESTION: Clearly the paintXfrm and init methods must delegate to all the composed paint strategies.   But what about the paint method?  Since the paint method of any paint strategy  is defined as creating any affine transform it needs, how can we avoid having multitudes of identical affine transforms being computed when a multi-paint strategy is asked to paint?   Hint: consider the services MultiPaintStrategy receives from its superclass. 

 

ADecoratorPaintStrategy

This class essentially inserts an indirection layer above its decoree, allowing the various methods to intercept their calls and perform additional behaviors, such as resetting the drawing color so that something always draws with the same color.     This is an example of the Decorator Design Pattern, which has uses from adding new GUI features to transparent system monitoring.

 

ImagePaintStrategy

Painting images is straightforward, thought there are some hoops that one has to jump through to get them loaded and sized properly.   The a main problem with images is that they are platform-dependent, in particular, they depend on the graphics subsystem in use.  Another problem is that they can be very large, memory-wise, so their loading and manipulation times may be significant, forcing the system to do other operations in the meanwhile but then requiring that many operations be aware that the image may not yet be ready for further processing.

ImageObserver

Because of the synchronization issues that may arise with the asynchronous loading and other operations on images, a java.awt.image.ImageObserver object is often needed handle that synchronization.   Luckily almost all GUI components implement the ImageObserver interface.   The best image observer to use is the component upon which the image will be drawn, i.e. the drawing canvas.   

Unfortunately, the image observer isn't available until the paint strategy's init() method is run, meaning that any operations involving the image observer must be delayed at least until the init() method is run.   The init() method should also save the image observer in a field for easier access.

Loading images

The java.awt.Image class is used to represent an image in the system.    While there are simpler techniques to load images off of the disk, because of Java's "sandbox" security rules when Java code is running on the web which restrict disk access, not all file loading techniques will work in all situations.  In particular, we want a technique that will load the images either from the disk or from a JAR file, as would be used in an applet or other simple deployment.  

What we need to do is to invoke the resource loading mechanism for the current Java system, which understands how to load classes and files from whatever is being used to run the current Java application or applet.   This mechanism knows whether to look through the file system on the disk or through the JAR file as appropriate because it already handled the loading of all the classes that are currently running.  Thus we can accomplish a transparent loading process that is independent of how we started our application/applet.  

Every Object in Java is capable of retrieving the Class object that represents the class of that object.  That Class object can access the platform-specific resource loader that was used to load itself.  

this.getClass().getResource(filename) will actually return a URL of the file location, which is usable for either the file system or a jar file or a web location.   filename is the file plus its path relative to the location of the class that is doing the loading, i.e. the ImagePaintStrategy.  Since we are working with URL's, you need to use a forward slash as the directory separator.   For instance, if the images are stored in a directory below the paint strategy directory, we would have something like filename = "images/myImage.jpg".

If you search the web, you will see various methods of actually loading the image, given its URL.   I have personally had mixed results with the various methods I've seen.   The technique that I've found that has worked well over the years is a little wasteful in that it makes a javax.swing.ImageIcon object that it promptly discards, but it does seem to work well.   Basically, you use the URL to instantiate the ImageIcon and get the Image from inside of it.  If you can find a better method, please let me know!

image = new ImageIcon(this.getClass().getResource(filename)).getImage();

A more "correct" but more complicated technique to load the image is discussed below.

Scaling the Image

A very practical issue with images is that the "working" part of the image may not be considerably smaller than the actual width and height of the image itself.  The is particularly an issue when trying to define a boundary for collisions.   Here's we are using a very simple circular collision radius, so we need to define a "radius" for the image that will define its collision boundary.  Since we are creating a unit-sized prototype image, the easiest way to specify to specify this radius, the "fill factor", in in terms of a percentage of (ratio) the average of the width and height the image.  Thus a fill factor of 0.75 means that the bounding circle is 75% of the extent of the total image.

The net scale factor that will transform the image into size such that the bounding circle is of unit size is thus the inverse of the fill factor times the average of the width and height:

scaleFactor = 1.0/(fillFactor*(image.getWidth(imageObs)+image.getHeight(imageObs))/2.0);

Note that the fill factor is available to the constructor of the ImagePaintStrategy while the scale factor cannot be computed until the paint strategy is initialized by the host ball because the image observer is not available until then.

Painting the image

Unfortunately, there is no simple mechanism to pre-scale and offset an image.   There are some techniques involving BufferedImage but they don't seem to work with animated GIFs.  another alternative is to create an affine transform that will do the scaling and offsetting needed to create a unit sized image.   But we need to be careful because we do not want to modify the globally used affine transform being handed to paintXfrm with scaling and offset data specific to this image (remember that this paint strategy may be part of a larger composition).

The solution is to hold a local affine transform object and combine it with the global affine transform object being handed to paintXfrm.   

The job of paintXfrm is thus to take the local affine transform, configure it to offset the image to center it,  scale the image as per the scaling factor and then perform the transform of the resultant unit image to its place on the screen. Finally, the image is drawn on the screen using the newly aggregated affine transform.

It is important to remember that since translation does not commute with scaling and rotation (i.e. order of operation matters), we need to be very careful as to which order we modify the local affine transform.   In general, the AffineTransform will perform the operations in the reverse order in which the modifications are made to it.    Thus, we first reset the transform to be scaling only, then we add the translation to the center of the image.   Since we want whatever the incoming affine transform does to be applied after the local transform, we use the preConcatenate method to insure this order of execution.   The fully loaded local affine transform is now handed to the Graphic2D object's drawImage method along with the image and the ImageObserver to be rendered onto the screen.

   protected AfffineTransform localAT = new AffineTransform();
	
  public void paintXfrm(Graphics g, Ball host, AffineTransform at){
	localAT.setToScale(scaleFactor, scaleFactor);
	localAT.translate(-image.getWidth(imageObs)/2.0, -image.getHeight(imageObs)/2.0);
	localAT.preConcatenate(at);
	((Graphics2D)g).drawImage(image, localAT, imageObs); 
  }

 

Here's a couple of animated GIFs to start you out with (go find some more!).   The BirdSheepImagePaintStrategy in the demo merely randomly chooses between one of these two images when it instantiates.  A 50% fill factor is reasonable for the images.

  

Note that displaying animated GIFs is not the same as creating an animated ball under programmatic control.   An animated GIF is a single image that animates all by itself, without any intervention by your code.

The code as described is for images that are centered at the origin.   What changes would you make to support images that are off-center, as would be used in a composite image painting strategy?   Watch the order of operations in the transforms!

 

Keeping an image or shape upright

Since APaintStrategy uses Math.atan2(velocity.y, velocity.x) to calculate the rotation angle for the image or shape, the rotation angle is always -PI < angle < PI.    If we say that the unit shape or image is "pointing" to the right, then any rotation angle greater than +PI/2 or less than -PI/2 would mean that the image or shape would be upside down.  To correct this, we use the paintCfg method to add an additional scaling correction, a flipping of the Y-axis, whenever the rotation angle means that the sprite is travelling to the left:

  protected void paintCfg(Graphics g, Ball host) {
    super.paintCfg(g, host);
    if(Math.abs(Math.abs(Math.atan2(host.getVelocity().y, host.getVelocity().x)))> Math.PI/2.0) {
      at.scale(1.0, -1);
    }        
  }

Notice how the superclass's paintCfg is called to insure that the superclass behavior, if any, is not lost.

The question is "what classes need this paintCfg such that only the classes that need this behavior are affected?"

 

AnimatePaintStrategy

What is animation anyway?  If you want a ball to be changing the way it looks, e.g. a fish opening and closing its mouth, how does that relate to paint strategies?  Therefore, what does AnimatePaintStrategy do?   How could you improve the design to enable one to change the animation rate?


Variable Input Argument Lists ("Vararg")

Varargs are a very useful addition to the Java language that allows an unspecified (0...*) number of a single type of input parameters to be used with calling a method.   The basic syntax of a method using varargs is (optional non-vararg inputs also shown):

AType aVarargMethod(Type1 x, Type2 y, VarargType... vArgs) {
	int numVarargs = vArgs.length;
	VarargType ithElement = vArgs[i];  // 0 <= i < numVarargs  

Essentially the varargs appear as an array of objects of the specified type.  Note that fundamentally, varargs must appear as the last input parameter of a method.  The length of the array is the number of varargs the method was called with.

To call a vararg method, simply put in as many vararg parameters as you wish, including none.  All the following calls are valid:

aVarargMeth(x, y);
aVarargMeth(x, y, a);
aVarargMeth(x, y, a, b);
aVarargMeth(x, y, a, b, c);

A More "Correct" Method of Loading Images

An arguable more proper image loading technique is to use the image loading capabilities in the Java system's "toolkit"  (java.awt.Toolkit).  This technique leverages Java's built-in capabilities to asynchronously load potentially resource-intensive images.   Unfortunately, this means that one has to be careful to insure that images are completely loaded before attempting to work with them, e.g. get their widths and heights.

In the constructor, instead of using an ImageIcon to process the loading, we use the services provided by the current Toolkit:

  image = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource(filename));

In the init() method, we need to wait for the image to load using a java.awt.MediaTracker before we can calculate the scale factor:

  public void init(Ball host) {
    imageObs = host.getEnv().getCanvas();
    MediaTracker mt = new MediaTracker(host.getEnv().getCanvas());
    mt.addImage(image, 1);
    try {
    	mt.waitForAll();
    }
    catch(Exception e){
    	System.out.println("ImagePaintStrategy.init(): Error waiting for image.  Exception = "+e);
    }

    scaleFactor = 1.0/(fillFactor*(image.getWidth(imageObs)+image.getHeight(imageObs))/2.0);
  }

 

 


© 2010 by Stephen Wong