When we went to the unit square implementation of PatterningSheet and ShotPattern, we stopped after the FitNesse tests ran, but before we modified our image-drawing spikes. Here, we complete that implementation and clean up the code as we do it.

Getting Things Done

After we changed to the unit square implementation, our image-drawing programs no longer worked. I’ll spare you the pictures: basically the entire drawing collapsed to a point in the top left of the picture, since the unit square is a pretty tiny part of a 2048x1526 picture. So we needed to get that code to work, and while we were at it, we wanted to make it a bit better.

It’s worth calling out this practice. Whenever a piece of code needs maintenance, it’s a good idea to schedule some of the clean-up that the code needs. Since our picture drawing code was very ad hoc, but quite simple, we were sure we could do the job quickly. Let’s take a look at where we started. Here’s our picture of the center of mass of the shot:

image

The code for that image looks like this:

package com.hendricksonxp.patterning.model;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;

public class CreatePatternImage {

    final String folder = "Data\\";

    public static void main(String[] args) throws IOException  {
        CreatePatternImage image = new CreatePatternImage();
        image.doit();
    }

    public void doit() throws IOException { 
        RenderedImage rendImage = patternImage();    
        File file = new File(folder+"patternImage.png");
        ImageIO.write(rendImage, "png", file);    
        file = new File(folder+"patternImage.jpg");
        ImageIO.write(rendImage, "jpg", file);
    }

    public RenderedImage patternImage() {
        int width = 2048;
        int height = 1536;

        BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);    
        Graphics2D g2d = bufferedImage.createGraphics();
        g2d.setColor(Color.white);
        g2d.fillRect(0, 0, width, height);
        // g2d.translate(1024, 768);
        // g2d.scale(1, -1);

        ShotPattern pattern = new ShotPattern(folder+"PB270011.bmp");

        g2d.setColor(Color.GREEN);
        g2d.fillOval((int) pattern.centerOfMass().x()-75, (int)pattern.centerOfMass().y()-37, 150, 74);

        g2d.setColor(Color.BLACK);
        g2d.setStroke(new BasicStroke(3));
        g2d.drawLine(1024-25, 784, 1024+25, 784);
        g2d.drawLine(1024,784-25,1024,784+25);
        g2d.setColor(Color.RED);
        for (Hit hit: pattern.hits) {
            g2d.fillOval((int)hit.x(), (int)hit.y(), 10, 10);
        }

        g2d.dispose();    
        return bufferedImage;
    }   
}

Now we can see two basic problems. The highlighted code above is an example of both. First, there are little chunks of code that have meaning. The highlighted code above draws the hit dots, in red. But that code is just in-line with code that has other functions. So it should be pulled out into a method of its own, somewhat like this:

   private void drawHits(Graphics2D g2d, ShotPattern pattern) {
        g2d.setColor(Color.RED);
                for (Hit hit: pattern.hits) {
                    g2d.fillOval((int)hit.x(), (int)hit.y(), 10, 10);
        }
    }

The second issue, of course, is that the code doesn’t do what we want. The Hits’ x and y coordinates are now fractions between zero and one, not integers between 0 and 2048 or 1536. So all the hits draw themselves in the top left corner. If you were using a pen, you’d wear a hole in the paper.

A moment’s reasoning told us that the change needed was simple. Since the internal x-value of a Hit is a fraction between zero and one, and since the output value is between zero and the width of the picture, the center of the hit, in the picture, should be at x*width. The width, in our case, is of course 2048. We brought those values out to the field level, along with some file and folder names:

public class CreatePatternImage {

    final String outputFolder = "Output\\";
    final String inputFolder = "Data\\";
    final int width = 2048;
    final int height = 1536;

We weren’t sure that this was the best idea, but we were sure it was better than writing “*2048” explicitly all over.

We also decided to improve some things. For example, in the code above, the dots for the hits are red, and they aren’t drawn centered around the hits, but instead their bounding rectangle has its origin at the hit. It did the job of showing that we could draw the picture, but it’s not accurate. In addition, we discovered that we preferred black dots for our final picture, and we decided we would do well to factor out the radius of the dots. The resulting code was this:

   private void drawHits(Graphics2D g2d, ShotPattern pattern) {
        int shotRadius = 4;
        g2d.setColor(Color.BLACK);
        for (Hit hit: pattern.hits) {
            g2d.fillOval((int)(hit.x()*width) - shotRadius, (int)(hit.y()*height) - shotRadius,
                    shotRadius*2, shotRadius*2);
        }
    }

Now this does what we want, and it’s broken out into a separate method. That’s good. What’s not so entirely good is that there’s a lot of subtracting of the radius, multiplying the radius by two, and so on. At this point, we think it’s a little bit more clear in some ways, and uglier in others. We’ll keep an eye out for how to make it better yet, but we prefer it as it is over where it was all in line and multiplying and adding apparently meaningless constants.

It turns out that we changed almost every single part of the drawing in some detail. Chet wanted to use our prettier target with the crosshairs and rings, and he wanted to use a smaller round indicator of the center of mass, as we used in our other picture, this one:

image

We made all those changes in the same three steps:

  1. Extract the method to do one piece of the picture;
  2. Make the method work with unit square input;
  3. Change the appearance of the piece.

Naturally, as we did the steps, we tended to get more casual with experience, and then we’d have a picture with something horribly wrong with it. Then we’d go back to step by step, and get it right. If we were kids, we’d each have been sending the other one out on the new ice on the lake, to see when he would break through. Instead, we broke the code a bit, then backed up and made it work.

The Result

The resulting picture looks like this:

image

And here’s the code as it stands today:

public class CreatePatternImage {

    final String outputFolder = "Output\\";
    final String inputFolder = "Data\\";
    final int width = 2048;
    final int height = 1536;

    public static void main(String[] args) throws IOException  {
        CreatePatternImage image = new CreatePatternImage();
        image.doit();
    }

    public void doit() throws IOException { 
        RenderedImage image = drawPointOfImpact();    
        File file = new File(outputFolder+"patternImage.jpg");
        ImageIO.write(image, "jpg", file);
    }

    public RenderedImage drawPointOfImpact() {
        ShotPattern pattern = new ShotPattern(inputFolder+"PB270011.bmp");    
        BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);    
        Graphics2D g2d = bufferedImage.createGraphics();
        clearImage(g2d);
        drawCenterOfPattern(g2d, pattern);
        drawTarget(g2d);
        drawHits(g2d, pattern);
        g2d.dispose();    
        return bufferedImage;
    }

    private void clearImage(Graphics2D g2d) {
        g2d.setColor(Color.white);
        g2d.fillRect(0, 0, width, height);
    }

    private void drawHits(Graphics2D g2d, ShotPattern pattern) {
        int shotRadius = 4;
        g2d.setColor(Color.BLACK);
        for (Hit hit: pattern.hits) {
            g2d.fillOval((int)(hit.x()*width) - shotRadius, (int)(hit.y()*height) - shotRadius,
                    shotRadius*2, shotRadius*2);
        }
    }

    private void drawTarget(Graphics2D g2d) {
        g2d.setColor(Color.RED);
        g2d.drawOval(width/2-20, height/2-20, 40, 40);
        g2d.drawOval(width/2-10, height/2-10, 20, 20);
        g2d.setStroke(new BasicStroke(3));
        g2d.drawLine(width/2-25, height/2, width/2+25, height/2);
        g2d.drawLine(width/2,height/2-25,width/2,height/2+25);
    }

    private void drawCenterOfPattern(Graphics2D g2d, ShotPattern pattern) {
        int centerRadius = 15;
        g2d.setColor(Color.RED);
        g2d.fillOval((
                int) (pattern.centerOfMass().x()*width)-centerRadius, 
                (int)(pattern.centerOfMass().y()*height)-centerRadius, 
                centerRadius*2, centerRadius*2);
    }   
}

What's [Not to] Like

This program is better factored, in that it has separate methods for the various things it draws. The code is now independent of the particular height and width of the page it draws, and it’s all lined up with the unit square implementation of the model. That’s good.

On the other hand, we clearly need a circle-drawing method to encapsulate all those subtracts and multiplies, and it would be nice to have something to factor in the width and height factors. Some of this can probably be done by putting some transforms into the Graphics2D instance, and some will perhaps want to be done with some private circle-drawing facility.

But that’s for another day. For today, the code is better – and more important: it works again!

Coming up next: an actual PDF report!