COMP 310
Spring 2019

Lec12:  Using Superclasses as Service Providers

Home  Info  Canvas   Java Resources  Eclipse Resources  Piazza

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 FishWorld system.  For reference, please see the FishWorld demo.  

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.   Also, there are some areas of this design that are distinctly debatable--these were deliberately left in to promote debate.

Preface: Things to look for in the design:

The architecture below may seem a bit complicated but it simplifies if we consider the following different points of customization for a strategy, i.e. places that a strategy can assert its variant nature:

  1. Customize upon installation:  This is an variant initialization process that a particular strategy uses to connect itself to and set itself up to process a particular host (context) into which the strategy is being installed.  This process would only happen once, when the strategy is first installed into its host (context).
  2. Customize upon invocation:  This is a variant customization process that occurs every time the strategy is invoked by the host.   This enables the strategy to insert variant behavior into the middle of the invariant behaviors, e.g. those behaviors driven by the strategy's superclass.  
  3.  Customize the final part of the behavior being invoked: This is a variant customization of the last part of the overall process being invoked.   This is the part typically associated with the variant behavior of the strategy in terms of being "what this particular strategy does".   This is where a particular strategy gets the final, ultimate control over the net behavior being invoked.

Typically, template design pattern processes are used to insert the above customizations into the middle of the superclass's invariant behaviors.

Look at the various abstract methods in the paint strategy architecture and analyze their role in the painting process in terms of the 3 different types of behavior customization above.


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, the concrete implementations for any branch of the hierarchy are not 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

Click here for the documentation for this code.

IPaintStrategy

The top-level superclass (interface, actually) that fundamentally defines what a painting strategy can do.    This is the view of the painting strategy from the perspective of the ball.

void paint(Graphics g, Ball host)  -- The actual paint operation that is called during the (re)painting process.  Paints the host onto the given Graphics context. The exact nature of the manipulations required to get the ball's visual representation onto the screen is up to the implementing class and may or may not involve affine transforms.

void init(Ball host) -- used to initialize the strategy and host ball.   This method must be run whenever the ball gets a new strategy, such as in a setPaintStrategy method or even in the constructor of the ball.   The safest way to do this is to have the constructor set the paint strategy field by calling the setPaintStrategy method thus keeping the code to initialize the strategy only in a single location.

A simple, direct implementation of IPaintStrategy can be seen in SquarePaintStrategy, which does not rotate the square as it bounces around.   This strategy simply uses a built-in method of the Graphics object to paint a filled square (rectangle) onto the screen.    No affine transforms are involved.  

package ballworld.model.paint.strategy;
import java.awt.*;
import ballworld.model.*;

/**
 * Paint strategy that paints a filled square with the Ball's radius.
 * This functionality is duplicated by the RectanglePaintStrategy.
 * The class demonstrates a direct implementation of IPaintStrategy.
 */
public class SquarePaintStrategy implements IPaintStrategy {
  
  /** 
   * No parameter constructor for the class
   */
  public SquarePaintStrategy() {
  }

  /**
   * Paints a square on the given graphics context using the color and radius 
   * provided by the host. 
   * param g The Graphics context that will be paint on
   * param host The host Ball that the required information will be pulled from.
   */
  public void paint(Graphics g, Ball host) {
      int halfSide = host.getRadius();
      g.setColor(host.getColor());
      g.fillRect(host.getLocation().x-halfSide, host.getLocation().y-halfSide, 2*halfSide, 2*halfSide);
  }
  
  /**
   * By default, do nothing for initialization.
   */
  public void init(Ball context){
  } 
}

 

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.    This class is designed to be the root class for all strategies that use affine transforms to create their visual representations.

One convenience service that APaintStrategy provides for all of its subclasses is default behavior for some of its methods.  The subclasses only need to override those methods that it needs withouth having to explicitly implement the behaviors of all possible methods.

Default Behavior Convenience Class

It is important to emphasize that this class provides default behaviors as a convenience to its subclasses.    It does not create an abstract or concrete definition of those behaviors.  That job was done by the top-level interface IPaintStrategy.  

In general the best way to add default behaviors to a system is to create a top-level interface that defines the abstract semantics of the operations and then to implement that interface with an abstract class whose methods provide the desired default behavior. 

With this type of architecture, a developer is free to implement the interface with different default behaviors without interfering with the semantics of either the top-level interface or the existing default behavior provider.

The constructor for APaintStrategy 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.   Notice that the subclasses of APaintStrategy often have two constructors, one that has no input parameters and one that takes an AffineTransform object.   This allows the flexibility of using the class in isolation, where the AffineTransform is created during its own construction (the no-parameter constructor) or to use an existing AffineTransform  (the one-parameter constructor) when the strategy is being used in conjunction with other strategies and thus must be locked to the same transformations.

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(host.getVelocity().x, host.getVelocity().y);
    g.setColor(host.getColor());    
    paintCfg(g, host);
    paintXfrm(g, host, at);
  }
  

protected void paintCfg(Graphics g, Ball host) -- The paintCfg method is set to be a concrete no-op that the subclasses may or may not override.   This method allows the subclass to inject additional processing into the paint method process before the final transformations are performed.     Since this method is "protected", it is only available for use by the subclasses and not other types of objects.

public abstract void paintXfrm(Graphics g, Ball host, AffineTransform at) --  A secondary paint operation that is the last step of the above paint method, which does not calculate its own affine transform, but instead, uses a supplied affine transform.   Notice that the translation, rotation and scaling have already been added to the affine transform before it gets to paintXfrm.  This allows the same affine transform to be shared amongst paint strategies, reducing the number of times that it has to be calculated.   When an affine transform instance is being shared amongst strategies, then it is invariant across those strategies.  Thus, this method allows an invariant translation, rotation and scaling to be performed (previously in the paint method) that applies to all composed strategies.    But as we have seen before, invariant properties cause problems when we try to compose entities together as those invariant properties tend to "run into each other" and make composition very difficult.   This method does not include the invariant parts of the affine transform process, so affine transform-based paint strategies can be combined based on this method but not based on the paint method.  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. 

 

The difference between paintCfg and paintXfrm

On the surface, paintCfg and paintXfrm look almost identical, especially when you consider that the affine transform, at, that is being passed to paintXfrm is a field of APaintStrategy.   Since the paint method is a template method, why should one need 2 potentially overridden methods to delegate to for custom processing?   But the key is to consider who is expected to call each method.

paintCfg is designed to be only called by the superclass, that is, it is always a participant in the template method pattern of APaintStrategy.   It enables a subclass of APaintStrategy to provide custom configuration services to itself.

paintXfrm, on the other hand, is designed to be called either by its own APaintStrategy superclass or by another paint strategy.   It provides the actual, concrete visual rendering service of its class to whomever is calling it.    The place we see this most strikingly is in a subclass of MultiPaintStrategy.  In this composite strategy, we may wish that the image produced by the composited paint strategy remain in an upright orientation no matter what direction they are travelling.  The NiceFish paint strategy does this, for instance.   On the other hand, all the composee strategies need to use the same affine transform.   Thus, the MultiStrategy's paintXfrm method delegates to all the composees, passing a common affine transform to all.   The MultiStrategy's paint method does NOT delegate to the composees.   It instead delegates first to its own (overridden by any subclass) paintCfg, which will augment the common affine transform with a vertical reflection (y scaling by -1) and then it delegates to its own paintXfrm, which does the delegation to the composees.

Is there a better way to do this?   A very good question.   Use all the design ideas and principles covered so far to come up with alternative techniques to solve these problems that at least for some situations could be more advantageous or easier or simpler than this technique.

 

ShapePaintStrategy

This class is very similar the SquarePaintStrategy, but abstracts out the shape aspects into an java.awt.Shape object transformed by an affine transform.   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 which uses the existing affine transform to create the shape image at the desired size, rotation and location:

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

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.  All any subclass of ShapePaintStrategy needs to do is to supply a unit-sized Shape to paint.  

But the ShapePaintStrategy and it subclasses really don't care how the shape is created, just so long as they have a shape to work with.  Besides, that same shape might be used elsewhere as well, so this means that the actual shape construction should be encapsulated in a factory (see below).

An example of a ShapePaintStrategy subclass is the EllipsePaintStrategy, which is essentially what was done in lab, except that there the shape was hard-coded into the ball class by you.

package ballworld.model.paint.strategy;
import java.awt.geom.AffineTransform;
import ballworld.model.paint.ShapePaintStrategy;
import ballworld.model.paint.shape.*;

/** 
 * Paint strategy to paint an ellipse shape
 */ 
public class EllipsePaintStrategy extends ShapePaintStrategy {
  
  /**
   * No parameter constructor that creates a prototype ellipse that 
   * has twice the width as height but an average radius of 1.
   * An AffineTranform for internal use is instantiated.
   */
  public EllipsePaintStrategy(){
    this(new AffineTransform(), 0, 0, 4.0/3.0, 2.0/3.0);
  }
  
  /**
   * Constructor that allows the specification of the location, x-radius and y-radius
   * of the prototype ellipse.   The AffineTransform to use is given.
   * @param at The AffineTransform to use for internal calculations
   * @param x floating point x-coordinate of center of circle
   * @param y floating point y-coordinate of center of circle
   * @param width floating point x-radius of the circle (ellipse)
   * @param height floating point y-radius of the circle (ellipse)
   */
  public EllipsePaintStrategy(AffineTransform at, double x, double y, double width, double height){
    super(at, EllipseShapeFactory.Singleton.makeShape(x,y,width,height));
  }
}

 

Shape Factories

Since the affine transform-based painting of Shapes does not depend on what that shape is, shape factories are used to to create the unit-size prototypes needed by the affine transform.

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

BallWorld Shape Factories

These factories create the prototype shapes for use in the Prototype Design Pattern painting process, 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 problem with Polygons is that they require integer point values.   Thus it is essentially impossible to create a unit sized polygonal shape.\

So, the polygon factory holds an internal prototype polygon which is defined at whatever size, location and orientation is convenient using integer point values.   The output of the factory is a transformation of that prototype polygon that uses the scale factor to change the size of the polygon to something that is consistent with the size of a circle of radius 1.0.

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, a floating point double scale factor is also 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 (plus any desired rotation) 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);  // optional rotation can be added as well
    return at.createTransformedShape(poly);
  }

The PolygonFactory produces a unit size prototype Shape object just like any other IShapeFactory,

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

This Composite Design Pattern paint strategy composes affine transform-based paint strategies, i.e. APaintStrategies.    It will not compose general IPaintStrategy's.   IPaintStrategies can be composed just like was done with the IUpdateStrategies before.   This type of composite is not shown on the diagram above.    This MultiPaintStrategy however addresses the problem of how to compose together affine transform-based paint strategies that have an invariant component (the affine transform itself) that needs to be shared across all the composees.   It does so not by delegating the paint method, but by delegating the paintXfrm method, which has the invariant portions of the transformations stripped away.

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.

Images are different than Polygons and other Shape objects because they cannot be transformed into a unit sized object.   Instead, the Java GUI subsystem uses the AffineTransform to convert the pixel locations in the image into pixel images on the screen--no free-standing object is every made.     This is in contrast to the ShapePaintStrategy which uses the AffineTransform to create another Shape object which what is painted to the screen.    The problem is compounded by the fact that the useful part of an image may not fill the entire bounds of the image, so a scaling is needed to compensate for this less than 100% "fill factor".   In the end, what we see is layers of scaling operations to get the image to the desired size in a well-controlled series of steps.

NOTE:

The IATImage interface in the provided code enables the ImageObserver and MediaTracker object to be encapsulated in an Image-like entity that decouples the model from those view-associated elements. This will greatly simplify the code in the image paint strategies and decouple them from having to deal with a component from the view as the code below describes. In order to achieve proper model-view decoupling, the IATImage must be instaniated in the model-to-view adapter (whose code is physically located in the controller since we are using anonymous innner classes for the adapters), not in either the model or the view. The provided code documentation describes multiple ways in which the construction of an IATImage object can be achieved.

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.

Note:  Some students have reported that their image painting code works fine even when the ImageObserver values is set to null. It is unclear if this is the expected behavior from the latests JRE versions or not. To be safe, it is recomended that one still use non-null values for the ImageObserver. If anyone can find official documentation stating whether null values are acceptible or not, please let the staff know right away.

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. your subclass of 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". (Note the use of the forward slash as a directory separator character).   Also, be sure that Eclipse knows that the directory exists, i.e. can see it and its contents in its package explorer pane.    If the Eclipse doesn't see it, it doesn't get copied over to the actual directory tree holding the executables (the "bin" directory).   If the images folder is in what you think is the right location, but they don't show up in the package explorer, right click the project in the package explorer and select "refresh" until it shows it up.

Be sure that your image files get committed to SVN!   Eclipse/Subclipse doesn't always automatically commit non-code files, so you may need to manually force their upload.

The best technique I've found to load an image 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.    This technique works whether the file is in a true file system or a "simulated" file system such as found inside of a compressed file, e.g. a Java JAR file which is used to encapsulate an entire package tree structure in a single file.

In the constructor,  we use the services provided by the current Toolkit:

try {
	image = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource(filename));
}
catch (Exception e){
	System.err.println("ImagePaintStrategy: Error reading file: "+filename +"\n"+e);
}

What is important to understand is that the image is not actually loaded yet.  Only the resources to get it have been allocated.  We still need to load the image.   We will do this in the init method of the paint strategy because this is when the strategy first comes into the context of the ball it is servicing.  

In the init() method, we need to wait for the image to load (which, relative to other operations, takes a long time) using a java.awt.MediaTracker before we can calculate the scale factor:

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

    scaleFactor = 2.0/(fillFactor*(image.getWidth(imageObs)+image.getHeight(imageObs))/2.0); // this line is described below
  }

This temporally expensive operation is done in the init method so that it only happens once in the life of a strategy.    Remember that the init method must always be called whenever a ball gets a new strategy, including in its constructor!

WARNING:  Failure to wait for the image to load will result in zeros for the image's width and height!

Odd Error:   If the fillFactor field is not properly initialized, the code may still run without error, even though one would expect that it should generate a "divide-by-zero" error in the code scaleFactor calculation above.   The lack of proper initialization would lead to an infinite scale factor however and images that don't seem to paint at all.

NOTE: When using IATImage objects, all the init() method needs to do is to ask the rest of the system to instantiate the IATImage object from the Image object and then proceed to calculate the scale factor and possibly the "pre"-affine transform object that will transform the upper-left corner, non-unit size IATImage to a unit sized, origin-centered prototype. See the discussion below.

The Problem with Images

Like Polygons, images are fundamentally described in terms of integer parameters, i.e. their width and height in pixels.   But in addition, images are always defined with their upper left-hand corners being at (0, 0).   These issues are compounded by some resultant effects that are very difficult for a computer to handle:

For example, the following image is 963x557 pixels where the peacock is neither centered in the image nor does the circular approximation encompass the entire bird and is only a small fraction of the entire picture:

flying_peacock

Recentering the image

An off-center image can easily be remedied by using an image processing tool (e.g. GIMP) to crop and/or resize the image such that the desired center of the image is at the center of the picture.   This simplifies the calculations needed since the translation of the image to re-center it on the origin can be determined simply by the width and height of the image.   What this does is to reduce the centering process to an invariant process that depends only on the width and height of the picture, which are values the computer can easily read directly from the Image object.

The amount to translate the image to center it on the origin now does not require any adddition information and is thus simply: (-image.getWidth(imageObs)/2.0, -image.getHeight(imageObs)/2.0)

If one does not use pre-centered images as described above, then the origin-centering translation will depend on the pixel location of the center of the desired image, which must be supplied to the calculation by the programmer.

Below is the same image but cropped, resized and repositioned such that the peacock is now centered in the picture.   It has also been flipped over so that the peacock is flying in the postive x-direction so that the rotations will work properly.   This image is 720x200 pixels and has a fill factor of about 20%.

resized flying peacock

Practical note:  In general, you will probably want to use GIF or PNG files that have transparent backgrounds so that the empty areas of the image will not obscure anything that is being drawn behind them.   This also makes them easy to resize the file and reposition desired image in the file without messing up the background around the desired image.

 

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 reason for specifying a "fill factor" rather than specifying the scaling factor directly is that calculating the scaling factor requires intimate knowledge of the exact dimensions of the picture file as well as the exact size of the circular approximation of the desired image in the picture.  These values are difficult for a human to decypher from a picture file but easy for a computer to read.    On the other hand, the fill factor does not depend on the specific dimensions of the picture or the desired image, only on the ratio of their sizes.   This ratio is easy for a human to estimate simply by looking at the picture but is difficult for a computer to calculate.  

In effect, the use of a fill factor value decouples the user from the picture.

The net scale factor that will transform the image into size such that the bounding circle becomes one of unit radius size is thus twice the inverse of the fill factor times the average of the width and height (without the factor of 2 in the numerator, you would get a unit diameter, not unit radius):

scaleFactor = 2.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.

Constructing an ImagePaintStrategy

To construct an ImagePaintStrategy, only two values are needed which are typically provided by a specific concrete subclass of ImagePaintStrategy:

  1. The pathname of the image file, relative to location of the class that is doing the loading, i.e. the subclass of ImagePaintStrategy that is specifying the image file.
  2. The fill factor specifying the approximate ratio of the size circular approximation of the desired image to the size of the entire image file.

Note that intimate knowledge of the dimensions of the image file is NOT required!

However, if you are not using pre-centered image files, you must also provide the pixel location of the desired center of desired image so that the proper centering translation can be calculated.

 

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 AffineTransform 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); 
  }

Note:  The image is assumed above to be centered in the picture, otherwise the translation amount would be the negative of the location of center of the image in the picture.

NOTE: When using IATImage objects, the imageObs above is not needed because the associated ImageObserver is already bundled inside of the IATImage and thus it can perform its necessary operations without explicitly invoking the ImageObserver.

One should note that the lines that initially set the localAT above to enable it to transform the image to a unit sized, origin-centered prototype are actually invariant with respect the paintXfrm() process. That is, one could define an invariant "pre"-affine transform that holds the transformation which takes the upper-left hand corner, non-unit size image and converts it to an origin-centered, unit-sized prototype. See how the code below moves the invariant processes out of the highly variant paintXfrm() method and into the relatively more invariant init() method:

	/**
	 * An invariant "pre"-affine transform used to transform the image into its unit size
	 * and location.
	 */
	protected AffineTransform preAT = new AffineTransform();
	
	/**
	 * Temporary affine transform used to create the net virtual transformation from image to screen
	 * This transform will be reprogrammed every time the strategy paints.
	 */
	protected AffineTransform tempAT = new AffineTransform();	
	
	public void init(Ball host) {
		// Elided code: 
		// - make the IATImage object from the Image object
		// - calculate the scaleFactor from the IATImage
		
		preAT.setToScale(scaleFactor, scaleFactor); // Scale the image down to unit size.  Why do we do this last?
		preAT.translate(-atImage.getWidth()/2.0, -atImage.getHeight()/2.0);  // First, center the image on the origin, assuming the displayed center is at the center of the image file.
	}
	
	
	public void paintXfrm(Graphics g, Ball host, AffineTransform at) {
		tempAT.setTransform(preAT);   // Initialize the tempAT to be the preAT, i.e. copy the preAT into the tempAT
		tempAT.preConcatenate(at);  // Add the normal affine transform to the "pre"-affine transform.  The "preAT" will be applied first then the "at" when transforming an image.
		
		atImage.draw(g, tempAT);   // draw the IATImage image using the composed transform
	}

A deliberately unresolved issue in the code above is how does an image paint strategy effectively call all the way out to the model-to-view adapter in order to get the IATImage object constructed from the paint strategy's Image object? Beware of coupling your system together even within the model itself! Ballworld gets its power from its decoupling -- adding coupling just reduces its capabilities. Think in terms of what services that the image paint strategy or other strategies need and who could naturally provide those services. That is, who can a strategy talk to? Who are the only entities with whom a strategy should be able to directly communicate? To get what you want may require delegating through several layers of encapsulation but those layers are crucial for keeping your system maximally decoupled.

 

Animated Images

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.atan2(host.getVelocity().y, host.getVelocity().x))> Math.PI/2.0) {
      at.scale(1.0, -1.0);
    }        
  }

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);

What's insufficient/wrong about our composition-based system as it stands?

Take a look again at the FishWorld demo and look at its behavior when the screen is resized or animated GIFs are used.

 


© 2019 by Stephen Wong