We get all the tests running after scaling to the unit square, and clean up the code substantially. Full listings in this issue, so stand back when you open the page.

Prequel

It’s icy here in Michigan, and we’re getting started a few minutes late at today’s office, the Borders in Brighton. They still don’t offer free Internet. I hope we don’t need the Web, it’ll cost me $6 to sign in on t-mobile. It’s odd, but I actually care about that, despite just having spent $7.42 to buy our water and my iced Chai. And don’t even mention lunch!

We begin by making all our FitNesse tests run again. This is accomplished by putting scaling into the tests, or into the methods, since we have now scaled everything into the unit square. We decided to leave two convenience methods in the ShotPattern, for center of pattern in inches:

   public int centerOfMassXinches(int targetWidth) {
        double rawX = centerOfMass().x();
        return (int) Math.round(rawX * targetWidth);
    }

    public int centerOfMassYinches(int targetHeight) {
        double rawY = centerOfMass().y();
        return (int) Math.round(rawY * targetHeight);
    }

Except for that, we elected to change other FitNesse tests to refer to the unit square scale, or to scale directly as they may need to. We made the latter decision on the grounds that most of the scaling belongs closer to the view than in the model, so it is appropriate, at least for now, to leave it that way.

In doing this, we had to modify three of our four FitNesse tests, and most of the fixtures as well. People doing Agile with Customer Acceptance Tests often complain that when they make design changes, they have to change their acceptance tests more often than seems right. Changing three isn’t bad: changing a hundred would be bad.

We are inclined to answer that if a lot of FitNesse tests need to change, it’s evidence that a refactoring was missed, or that some new requirement has arisen. But if we do miss refactorings, or new requirements come along … well, yes. We have more work to do.

Although we have, perhaps, more think time in because of the fact that we only work a couple of hours a day, we still have no more than a couple of weeks of elapsed programmer time in this program, so that the fact that we only had to change a few tests might be a function of that. This would argue that getting the architecture right early on is important, and I think supports our view that building the architecture by trying it is not substantially more costly than figuring it out abstractly first.

Now, certainly, if we had gone directly to the current implementation, it would probably have taken less time. But it’s easy to look at our first couple of approaches and critique them – and that point is in our favor, not in favor of those who would argue for more abstract thinking. The reason is that we had concrete code that we could look at, and experiment with, to determine in a much more direct way what was better. In the abstract, we might still be arguing … and we might still be just as wrong as we were, and for that matter just as wrong as we are now.

Lessons

Today’s work went very smoothly. The program’s modules are nicely separated now, and there aren’t too many places where the same thing needs to happen in different places. And we cleaned up the code substantially, with very little effort, as you’ll see below if you choose to read the code details.

We observe that we have had FitNesse tests fail without JUnit tests failing first, and that we have had our picture drawing programs fail without JUnit tests failing, and perhaps without FitNesse tests failing either. We have not been as disciplined as we recommend with respect to always writing a JUnit test that shows a bug before fixing the bug. In some cases, we couldn’t think of one: if a FitNesse fixture needs to be changed, it’s not clear that there is a bug that a JUnit test would have helped with.

In any case, we’re resolving to push a little harder on having the JUnit tests detect everything first, and on keeping the code more modular and clean. As you’ll see below, there are still some issues.

We’ll go through the code by package, Model, Tests, and so on. Because the model code is the real system, we’ll start with it, and then show the tests, though we can imagine that you might want to start with the tests if you were going to take over the code. Here, we want to talk about the model.

What follows isn’t quite literate programming, but I’ll be thinking about whether a little documentation like this would be useful for a program that was going to be passed on.

Model Code

ShotPattern

ShotPattern contains a list of Hits, scaled to the unit square. It contains calculation methods for center of mass, and the ability to count hits inside a given rectangle, for density calculations. It includes convenience methods for converting center of mass values into scaled inch values. It’s possible that these should be somewhere else, but we don’t see a better place yet.

There are two constructors, one that takes a list of Hits, and one that takes a file name, which uses a PatterningSheet to get the hits. We recognize that this latter give ShotPattern a dependency on PatterningSheet, but we don’t see a way to get rid of that that makes the code better. There would need to be a factory method somewhere: this might as well be it.

ShotPattern still contains some spike code for wedges, for doing density calculations in a sector format instead of an array of rectangles.

public class ShotPattern {
    List<Hit> hits = new ArrayList<Hit>();

    public ShotPattern(String fileName) {
        PatterningSheet sheet = new PatterningSheet(fileName);
        this.hits = sheet.getHits();
    }

    public ShotPattern(List<Hit> hits) {
        this.hits = hits;
    }

    public Hit centerOfMass() {
        if (hits.size() == 0) return new Hit(0,0);
        return new Hit(xCenter(), yCenter());
    }

    private double yCenter() {
        double sum = 0;
        for (Hit hit : hits)
            sum += hit.y();
        return sum/hits.size();
    }

    private double xCenter() {
        double sum = 0;
        for (Hit hit : hits)
            sum += hit.x();
        return sum/hits.size();
    }

    public int[] analyzePolar() {   
        Wedge[] wedges = createWedges();
        for (Wedge wedge: wedges)
            for ( Hit hit: hits)
                wedge.tally(hit);
        int tally = 0;
        int[] result = new int[16];
        for (Wedge wedge: wedges)
            result[tally++] = wedge.count();
        return result;
    }

    private Wedge[] createWedges() {
        Wedge[] wedges = new Wedge[16];
        int wedge = 0;
        int deltaR = 30;
        double deltaTheta = Math.PI/4;
        for (int radius = 0; radius < 60; radius += deltaR)
            for (double theta = 0; theta < 2*Math.PI; theta += deltaTheta)
                wedges[wedge++] = new Wedge(radius, radius+deltaR, theta, theta+deltaTheta);
        return wedges;
    }

    public int centerOfMassXinches(int targetWidth) {
        double rawX = centerOfMass().x();
        return (int) Math.round(rawX * targetWidth);
    }

    public int centerOfMassYinches(int targetHeight) {
        double rawY = centerOfMass().y();
        return (int) Math.round(rawY * targetHeight);
    }

    public int countHits(Rectangle2D rectangle) {
        int count = 0;
        for (Hit hit: hits)
            if (rectangle.contains(hit.x(), hit.y()))
                count++;
        return count;
    }
}

PatterningSheet

PatterningSheet is the object that reads a bmp file and finds hits in it. It produces a list of Hit scaled to the unit square, and returns it. A single Hit is recorded at the location of the first pixel encountered in a clump of contiguous pixels, and all the contiguous pixels are considered to be part of that Hit. We note that finding the center of the clump might be considered better, but don’t see the value at this point. If it needed to be done, it would all be encapsulated here.

The logic to consume the whole clump works by turning pixels off in the input raster, and there is a method, wasZero() which returns a result and has a side effect. We prefer that implementation to any other we have found, so sue us.

The code for consuming the clump has an interesting twist: the loop on the list points is processing a growing list, as candidate points which are found to be on are added to the list, so that their neighbors can be examined as well.

public class PatterningSheet {
    private WritableRaster farian;
    private int[] pixelArray = null;
    private int[] whiteArray = new int[] {1};

    public PatterningSheet(String fileName) {
        farian = raster(fileName);
    }

    public List<Hit> getHits() {
        List<Hit> hits = new ArrayList<Hit>();
        int width = (int)widthInPixels();
        int height = (int)heightInPixels();
        for (int y = 0; y < height; y++)
            for (int x = 0; x < width; x++)
                if ((farian.getPixel(x, y, pixelArray)[0]) == 0)
                    hits.add(makeHit(y, x));
        return hits;
    }   

    private Hit makeHit(int y, int x) {
        Hit hit = new Hit(x/widthInPixels(), y/heightInPixels());
        consumeAdditionalPointsInThisHit(x,y);
        return hit;
    }

    private void consumeAdditionalPointsInThisHit(int x, int y) {
        ArrayList<Point> points = new ArrayList<Point>();
        points.add(new Point(x,y));
        for (int i = 0; i < points.size(); i++)
            for (Point point: candidates(points.get(i)))
                if (wasOn(point))
                    points.add(point);
    }

    private boolean wasOn(Point point) {
        int x = (int)point.getX();
        int y = (int)point.getY();
        if (x < 0 || y < 0) return false;
        if (x >= farian.getWidth() || y >= farian.getHeight()) return false;
        return wasZero(x,y);
    }

    private boolean wasZero(int x, int y) {
        int wasZero = farian.getPixel(x, y, pixelArray)[0];
        farian.setPixel(x, y, whiteArray);
        return wasZero == 0;
    }

    private List<Point> candidates(Point point) {
        List<Point> candidates = new ArrayList<Point>();
        int x = point.getX();
        int y = point.getY();
        for (int newX = x-1; newX <= x+1; newX++)
            for (int newY = y-1; newY <= y+1; newY++)
                if ( newX != x || newY != y)
                    candidates.add(new Point(newX, newY));
        return candidates;
    }

    private WritableRaster raster(String fileName) {
        BufferedImage img = null;
        try {
            img = ImageIO.read(new File(fileName));
        } catch (IOException e) {
        }

        WritableRaster raster = (WritableRaster) img.getData();
        return raster;
    }

    public double widthInPixels() {
        return farian.getWidth();
    }

    public double heightInPixels() {
        return farian.getHeight();
    }
}

Hit

Hit is little more than a class representing a point. It knows an x and y value, represented as double, and expected to be between zero and one, though in fact Hit will represent any point. There is at least one test that makes use of this fact, which troubles us just a little.

Hit can return a point’s x and y coordinates, or can return its polar coordinates r and theta. Points compare as equal if they are within epsilon of each other. The value of epsilon is currently set to 0.001. This facility is currently used only in a couple of tests, and should probably be removed.

public class Hit {
    private double x;
    private double y;
    final private double epsilon = 0.001;

    public Hit(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double x() {
        return x;
    }

    public double y() {
        return y;
    }

    public double r() {
        return Math.sqrt(x*x+y*y);
    }

    public double theta() {
        double theta = Math.atan2(y, x);
        if (theta < 0) theta += 2*Math.PI;
        return theta;
    }

    @Override
    public int hashCode() {
        final int PRIME = 31;
        int result = 1;
        result = PRIME * result + (int)x;
        result = PRIME * result + (int)y;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final Hit other = (Hit) obj;
        if (Math.abs(x-other.x()) > epsilon)
            return false;
        if (Math.abs(y-other.y()) > epsilon)
            return false;
        return true;
    }

    @Override
    public String toString() {
        return "Hit(" + x + "," + y + ")";
    }
}

RectangularGrid

This little class knows a pattern, and can count hits inside itself, using the pattern’s countHits method. It seems likely that this could be simplified a bit now, but when we wrote it, having an identifiable object to work with was valuable.

public class RectangularGrid {

    private double width;
    private double height;
    private ShotPattern pattern;
    private Rectangle2D rectangle;

    public RectangularGrid(ShotPattern pattern, double widthInInches, double heightInInches) {
        this.width = widthInInches;
        this.height = heightInInches;
        this.pattern = pattern;
    }

    public int hitsIn(int gridX, int gridY) {
        rectangle = new Rectangle2D.Double(gridX*width, gridY*height, width, height);
        return pattern.countHits(rectangle);
    }
}

Point

Point is a helper class for the PatterningSheet, and could be internal to it if we did that sort of thing.

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
}

HitCollection

HitCollection is a helper class for the DensityTest class, and should probably be replaced by an ArrayList. In fact, I’ll do that now: this class isn’t pulling its weight. I’ll leave the code so you can see how useless it was.

public class HitCollection {
    List<Hit> hits = new ArrayList<Hit>();

    public void addHit(double x, double y) {
        hits.add(new Hit(x,y));
    }

    public void addHit(int x, int y) {
        addHit((double)x, (double)y);
    }

    public List<Hit> hits() {
        return hits;
    }
}

Wedge

Wedge is a small class representing a sector of space, between two radii and two angles. It is intended for use similarly to our RectangularGrid object. It’s a helper class for the analyzePolar facility of ShotPattern, and may or may not survive. My guess is that it will. The hit counting in Wedge is done a bit differently from that in the rectangular case, and we should try to see to it that the two schemes work the same way.

public class Wedge {

    private double innerR;
    private double outerR;
    private double lowTheta;
    private double highTheta;
    private int count;

    public Wedge(int innerR, int outerR, double lowTheta, double highTheta) {
        this.innerR = innerR;
        this.outerR = outerR;
        this.lowTheta = lowTheta;
        this.highTheta = highTheta;
    }

    public void tally(Hit hit) {
        if (contains(hit))
            count++;
    }

    private boolean contains(Hit hit) {
        if (hit.r() < innerR) return false;
        if (hit.r() >= outerR) return false;
        if (hit.theta() < lowTheta) return false;
        if (hit.theta() >= highTheta) return false;
        return true;
    }

    public int count() {
        return count;
    }
}

Drawing Programs

We have some drawing programs. These are main programs, cribbed from some resource on the Web, that draw the example pictures you’ve seen. They look similar, and I’ll include them both below, for completeness.

These programs contain lots of duplication and very little expressive code. They are just hacked together to display some information to ourselves and our customers. We’ll get stories soon to commercialize the displays that Chet really wants, and we’ll clean up that code at that time. This might call for an abstract superclass with a Template Method pattern, Chet suggests. He could be right.

public class CreatePatternImage {

    final String folder = "Data\\";

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

    public void doit() {

    RenderedImage rendImage = patternImage();

    try {
        File file = new File(folder+"patternImage.png");
        ImageIO.write(rendImage, "png", file);

        file = new File(folder+"patternImage.jpg");
        ImageIO.write(rendImage, "jpg", file);
    } catch (IOException e) {
    }
    }

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

public class CreateShadedPatternImage {
    final int numberOfSquares = 20;
    final int rectangleWidth = 2048/numberOfSquares;
    final int rectangleHeight = 1536/numberOfSquares;

    final String folder = "Data\\";

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

    public void doit() {

    RenderedImage rendImage = patternImage();

    try {
        File file = new File(folder+"shadedPatternImage.png");
        ImageIO.write(rendImage, "png", file);

        file = new File(folder+"shadedPatternImage.jpg");
        ImageIO.write(rendImage, "jpg", file);
    } catch (IOException e) {
    }
    }

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

        grayBlobs(g2d, pattern);

        g2d.setColor(Color.RED);
        g2d.fillOval((int) pattern.centerOfMass().x()-15, (int)pattern.centerOfMass().y()-15, 30, 30);

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

        g2d.dispose();    
        return bufferedImage;
    }

    private void grayBlobs(Graphics2D g2d, ShotPattern pattern) {
        RectangularGrid grid = new RectangularGrid(pattern, rectangleWidth, rectangleHeight);
        for ( int row = 0; row < numberOfSquares; row++)
            for (int col = 0; col < numberOfSquares; col++)
                grayBlob(g2d, grid, row, col);

    }

    private void grayBlob(Graphics2D g2d, RectangularGrid grid, int row, int col) {
        int count = grid.hitsIn(col, row);
        int color = color(count);
        g2d.setColor(new Color(color));
        g2d.fillRect(col*rectangleWidth, row*rectangleHeight, rectangleWidth, rectangleHeight);
    }

    private int color(int count) {
//      int gray = (255 - count*4 + count/2) & 0xFF;
        int gray = (255 - count*6) & 0xFF;
        int rgb = (gray << 16) | (gray << 8) | (gray << 0);
        return rgb;
    }   
}

George Dinwiddie points out that the try/catch with the empty catch, which occurs in both these programs, is odious. Should be deleted. One way is as follows:

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

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

        file = new File(folder+"shadedPatternImage.jpg");
        ImageIO.write(rendImage, "jpg", file);
    }

I, for one, am happy enough with that, because there’s not much recovering from the inability to create those files, and this way the programs won’t just fail silently. YMMV and we’ll of course be open to correspondence on the subject. The change also gives me the chance to clean up the bizarre indentation in those programs.

FitNesse Fixtures

There are a few FitNesse fixtures, references to which you can see in the various FitNesse examples, including one that I’ll put here if I don’t forget. They are unremarkable, but here they are:

public class CenterOfPatternFixture extends ColumnFixture {
    final String folder = "..\\Data\\";

    public String inputFileName;
    public int targetWidthInches;
    public int targetHeightInches;

    public int fromLeft(){
        ShotPattern pattern = new ShotPattern(folder + inputFileName);
        return pattern.centerOfMassXinches(targetWidthInches);
    }

    public int fromTop(){
        ShotPattern pattern = new ShotPattern(folder + inputFileName);
        return pattern.centerOfMassYinches(targetHeightInches);
    }
}

public class CreatePatternFromFile extends ColumnFixture {

    public String fileName;
    public static int columns;
    public static int rows;
    final String folder = "..\\Data\\";
    public static ShotPattern pattern;

    public Boolean doIt(){
        pattern = new ShotPattern(folder + fileName);
        return true;
    }
}

public class CenterOfMass extends ColumnFixture {
    public double xCenter() {
        double result = CreatePatternFromFile.pattern.centerOfMass().x();
        return roundToFourPlaces(result);
    }

    public double yCenter() {
        double result = CreatePatternFromFile.pattern.centerOfMass().y();
        return roundToFourPlaces(result);
    }

    private double roundToFourPlaces(double result) {
        return Math.round(result*10000)/10000.0;
    }
}

public class RectangularDensity extends ColumnFixture {
    public int row;
    public int column;

    public int count() {
        ShotPattern shotPattern =     CreatePatternFromFile.pattern;
        double numberOfColumns  = 1.0/CreatePatternFromFile.columns;
        double numberOfRows     = 1.0/CreatePatternFromFile.rows;
        RectangularGrid grid2x2 
            = new RectangularGrid(shotPattern,numberOfColumns, numberOfRows);
        return grid2x2.hitsIn(column, row);
    }
}

It irritates me somewhat that these are not all called SomethingFixture, and since I’ve been making some small changes to the code, I’m tempted to change these. But I’d have to change the FitNesse wiki also, and that would be a digression from my task, to write this article. So I won’t.

This is a classic example of short-sighted thinking, isn’t it? We’re in a hurry to get something “done”, so we leave it done poorly. Hell, you’ve shamed me into it. I’ll do it, I’ll do it. Stop bugging me.

What!?!?!? The FitNesse tests all run. It must be that FitNesse, when it sees a reference to a fixture named Foo, looks for a class named FooFixture, as well as a class named Foo. The wiki doesn’t even need to be updated. Now that’s cool.

And a lesson. Sometimes doing it right isn’t as hard as we think. And now the world is a slightly better place. Woot!

Unit Tests

There are a bunch of unit test classes, using JUnit. I’ll list them all here and then see if there are comments to make below.

public class CenterOfMassTest {
    List<Hit> hits = new ArrayList<Hit>();

    @Before
    public void setUp() throws Exception {
    }

    @Test
    public void trivial() {
        ShotPattern shotPattern = new ShotPattern(hits);
        assertEquals(new Hit(0,0), shotPattern.centerOfMass());
    }

    @Test
    public void x4Y9() {
        addHit(new Hit(4,9));
        ShotPattern shotPattern = new ShotPattern(hits);
        assertEquals(new Hit(4,9), shotPattern.centerOfMass());
    }

    @Test
    public void centerOfMore() {            
        addHit(new Hit(-3,+2));
        addHit(new Hit(0, +2));
        addHit(new Hit(+3, +2));
        addHit(new Hit(-3,0));
        addHit(new Hit(0,0));
        addHit(new Hit(+3,0));
        addHit(new Hit(-3,-2));
        addHit(new Hit(0,-2));
        addHit(new Hit(+3,-2));
        ShotPattern shotPattern = new ShotPattern(hits);

        assertEquals(new Hit(0,0), shotPattern.centerOfMass());
    }

    @Test
    public void leftOfCenter() {                
        addHit(new Hit(-6,+2));
        addHit(new Hit(0, +2));
        addHit(new Hit(+3, +2));
        addHit(new Hit(-6,0));
        addHit(new Hit(0,0));
        addHit(new Hit(+3,0));
        addHit(new Hit(-6,-2));
        addHit(new Hit(0,-2));
        addHit(new Hit(+3,-2));
        ShotPattern shotPattern = new ShotPattern(hits);

        assertEquals(new Hit(-1,0), shotPattern.centerOfMass());
    }

    private void addHit(Hit hit) {
        hits.add(hit);
    }   
}

public class DensityTest {
    ShotPattern pattern;
    List<Hit> hits = new ArrayList<Hit>();

    @Test
    public void wedge0() throws Exception {
        Wedge wedge = new Wedge(0, 30, 0, Math.PI/4);
        assertEquals(0, wedge.count());
        wedge.tally(new Hit(20,5));
        assertEquals(1, wedge.count());
        wedge.tally(new Hit(25,5));
        assertEquals(2, wedge.count());
        wedge.tally(new Hit(30,10));
        assertEquals(2, wedge.count());
    }

    @Test
    public void wedge4() throws Exception {
        Wedge wedge = new Wedge(0, 30, Math.PI, 5*Math.PI/4);
        assertEquals(0, wedge.count());
        wedge.tally(new Hit(-20,-5));
        assertEquals(1, wedge.count());
    }

    @Test
    public void polarDensity() throws Exception {
        addHit(20,5); // Octant 0
        addHit(25,5);

        addHit(10,10); // 1
        addHit(10,20);

        addHit(-5,20); // 2

        addHit(-10,5); // 3
        addHit(-20,10);

        addHit(-20,-5); // 4
        addHit(-15,-10);
        addHit(-20,-10);

        addHit(-5,-20); // 5
        addHit(-5,-25);

        addHit(10,-20); // 6

        addHit(20,-10); // 7

        addHit(50,10); // 8
        addHit(50,20);
        addHit(30,10);

        addHit(30,40); // 9
        addHit(20,40);
        addHit(10,50);

        addHit(-10,50); // 10
        addHit(-10,40);
        addHit(-20,50);
        addHit(-20,40);

        addHit(-40,30); // 11
        addHit(-50,20);
        addHit(-40,10);

        addHit(-50,-10); // 12
        addHit(-40,-20);
        addHit(-40,-30);

        addHit(-30,-40); // 13
        addHit(-20,-40);
        addHit(-20,-50);
        addHit(-10,-50);

        addHit(10,-40); // 14
        addHit(10,-50);
        addHit(20,-40);
        addHit(20,-50);
        addHit(30,-50);

        addHit(50,-10); // 15
        addHit(40,-20);
        addHit(40,-30);

        pattern = new ShotPattern(hits);

        int[] densities = pattern.analyzePolar();
        assertEquals(2, densities[0]);
        assertEquals(2, densities[1]);
        assertEquals(1, densities[2]);
        assertEquals(2, densities[3]);
        assertEquals(3, densities[4]);
        assertEquals(2, densities[5]);
        assertEquals(1, densities[6]);
        assertEquals(1, densities[7]); 

        assertEquals(3, densities[8]);
        assertEquals(3, densities[9]);
        assertEquals(4, densities[10]);
        assertEquals(3, densities[11]);
        assertEquals(3, densities[12]);
        assertEquals(4, densities[13]);
        assertEquals(5, densities[14]);
        assertEquals(3, densities[15]);
    }

    private void addHit(int x, int y) {
        hits.add(new Hit(x,y));
    }
}

public class NominalPelletCountTest {

    @Test
    public void oneOunceNumberEight() {
        int oneOunceNumberEight = 410;
        assertEquals(410, oneOunceNumberEight);
    }
}

public class PatterningSheetTest {

    final String folder = "Data\\";

    @Test
    public void twoHitsOn7x12() {
        PatterningSheet sheet = new PatterningSheet(folder + "twoHitsOn7x12.bmp");
        List hitList = sheet.getHits();
        assertEquals(2, hitList.size());
    }

    @Test
    public void twoBigHitsOn7x12() {
        PatterningSheet sheet = new PatterningSheet(folder + "twoBigHitsOn7x12.bmp");
        List hitList = sheet.getHits();
        assertEquals(2, hitList.size());
    }

    @Test
    public void about1500() {
        PatterningSheet sheet = new PatterningSheet(folder + "PB270011.bmp");
        List hitList = sheet.getHits();
        assertEquals(773, hitList.size());
    }

    @Test
    public void holeTestFile() {
        PatterningSheet sheet = new PatterningSheet(folder + "holeTest.bmp");
        List hitList = sheet.getHits();
        assertEquals(22, hitList.size());
    }

    @Test
    public void tm1CroppedFile() {
        PatterningSheet sheet = new PatterningSheet(folder + "tm1Cropped.bmp");
        List hitList = sheet.getHits();
        assertEquals(1085, hitList.size());
    }

    @Test
    public void tm1SubsetFile() {
        PatterningSheet sheet = new PatterningSheet(folder + "tm1Subset.bmp");
        List hitList = sheet.getHits();
        assertEquals(46, hitList.size());
    }

    @Test
    public void fourCorners() {
        PatterningSheet sheet = new PatterningSheet(folder + "fourcorners.bmp");
        List hitList = sheet.getHits();
        assertEquals(4, hitList.size());
    }
}

public class RasterTest {

    final String folder = "Data\\";

    @Test
    public void x4y9File() {
        int[] sampleArray = null;
        BufferedImage img = null;
        try {
            img = ImageIO.read(new File(folder + "x4y9on7x12.bmp"));
        } catch (IOException e) {
        }

        Raster raster = img.getData();
        assertEquals(7, raster.getWidth());
        assertEquals(12, raster.getHeight());

        assertEquals(0, (raster.getPixel(4, 9, sampleArray))[0]);
        assertEquals(1, (raster.getPixel(5, 5, sampleArray))[0]);
    }

    @Test
    public void useRasterToCreateShotPatternBiggerFile() {
        ShotPattern  shotPattern = new ShotPattern(folder + "PB270011.bmp");
        assertEquals(new Hit(1139/2048.0,470/1536.0), shotPattern.centerOfMass());
    }

    @Test
    public void centerOfSmallPicture() {
        ShotPattern  shotPattern = new ShotPattern(folder + "x4y9on7x12.bmp");
        assertEquals(new Hit(4/7.0, 9/12.0), shotPattern.centerOfMass());
    }
}

public class Rectangle2DTest {
    Rectangle2D rect;
    Rectangle2D rect2;
    Double epsilon;
    Double nearlyOne;
    Double nearlyOneAndAHalf;

    @Before
    public void setUp() throws Exception {
        rect = new Rectangle2D.Double(0,0,1,1);
        rect2 = new Rectangle2D.Double(0.5,0.5,1,1);
        epsilon = Math.ulp(1.0);
        nearlyOne = 1.0 - epsilon;
        nearlyOneAndAHalf = 1.5 - epsilon;
    }

    @Test
    public void contains() {
        assertTrue(rect.contains(0.5,0.5));
        assertFalse(rect.contains(2,2));
        assertTrue(rect.contains(0,0));
        assertTrue(rect.contains(0,nearlyOne));
        assertTrue(rect.contains(nearlyOne,0));
        assertFalse(rect.contains(1,1));
    }

    @Test
    public void rectangleIsOpen() {
        assertFalse(rect.contains(0,1));
        assertFalse(rect.contains(1,0));
        assertFalse(rect.contains(1,1));        
    }

    @Test
    public void anotherContains() {
        assertTrue(rect2.contains(0.75,0.75));
        assertFalse(rect2.contains(2,2));
        assertTrue(rect2.contains(0.5,0.5));
        assertTrue(rect2.contains(0.5,nearlyOneAndAHalf));
        assertTrue(rect2.contains(nearlyOneAndAHalf,0.5));
        assertFalse(rect2.contains(1.5,1.5));
    }

    @Test
    public void anotherRectangleIsOpen() {
        assertFalse(rect2.contains(0.5,1.5));
        assertFalse(rect2.contains(1.5,0.5));
        assertFalse(rect2.contains(1.5,1.5));       
    }
}

public class RectangularDensityTest {

    private static final double GRIDHEIGHT = 1/3.0;
    private static final double GRIDWIDTH  = 1/4.0;
    public String fileName;
    final String folder = "Data\\";
    private ShotPattern pattern;

    @Test
    public void grid00() {
        pattern = new ShotPattern(folder+"tm1subset.bmp");
        RectangularGrid grid2x2 = new RectangularGrid(pattern,GRIDWIDTH,GRIDHEIGHT);
        assertEquals(1, grid2x2.hitsIn(0,0));
    }

    @Test
    public void grid32() {
        pattern = new ShotPattern(folder+"tm1subset.bmp");
        RectangularGrid grid2x2 = new RectangularGrid(pattern,GRIDWIDTH,GRIDHEIGHT);
        assertEquals(7, grid2x2.hitsIn(3,2));
    }

    @Test
    public void grid02() {
        pattern = new ShotPattern(folder+"tm1subset.bmp");
        RectangularGrid grid2x2 = new RectangularGrid(pattern,GRIDWIDTH,GRIDHEIGHT);
        assertEquals(2, grid2x2.hitsIn(0,2));
    }

    @Test
    public void grid30() {
        pattern = new ShotPattern(folder+"tm1subset.bmp");
        RectangularGrid grid2x2 = new RectangularGrid(pattern,GRIDWIDTH,GRIDHEIGHT);
        assertEquals(4, grid2x2.hitsIn(3,0));
    }
}

I don’t see anything exciting there. There’s that odd NominalPelletCount test, which Chet wrote to document a fact that’s important to him. You can imagine that there might be a constants page or something, but we do look at tests when we want to find things out, so he wanted to try documenting this little fact there.

Aside from that, the tests are mostly straightforward, and are included for completeness. At this point, you’ve seen the code as it is, except where I mentioned a change in the text and didn’t show the code a second time. Nothing very interesting there: you’ll see the changes the next time the class in question goes by, and I’ll wager you’ll not notice the differences.

The Tests

The tests are all green, as shown below:

image

image

image

We’re good to go. I’m checking in, and publishing this article. You’re up to date. Tomorrow, we’ll either plan our exciting new course for Agile2007, or perhaps work on Wedge density. Stay tuned, and keep those cards and letters coming in. Thanks!